为什么存储超过 BSS 末尾时没有出现分段错误?
Why didn't I get segmentation fault when storing past the end of the BSS?
我正在试验汇编语言并编写了一个将 2 个硬编码字节打印到标准输出的程序。这是:
section .text
global _start
_start:
mov eax, 0x0A31
mov [val], eax
mov eax, 4
mov ebx, 1
mov ecx, val
mov edx, 2
int 0x80
mov eax, 1
int 0x80
segment .bss
val resb 1; <------ Here
请注意,我在 bss 段内只保留了 1 个字节,但实际上将 2 个字节(1
和 newline
符号的字符码)放入内存位置。该程序运行良好。它打印了 1
个字符,然后是 newline
。
但我预计会出现分段错误。为什么没有发生。我们只保留了1个字节,却放了2个。
x86,像大多数其他现代架构一样,使用 paging / virtual memory for memory protection. 在 x86 上(再次像许多其他架构),粒度是 4kiB。
存储到 val
的 4 字节不会出错,除非链接器恰好将它放在页面的最后 3 个字节中,并且下一页未映射。
实际发生的是,您只是覆盖了 val
之后的内容。在这种情况下,它只是未使用 space 到页面末尾。如果您在 BSS 中有其他静态存储位置,您会踩到它们的值。 (如果你愿意,可以称它们为 "variables",但是 "variable" 的高级概念并不仅仅意味着内存位置,变量可以存在于寄存器中并且永远不需要地址。 )
除了上面链接的维基百科文章外,另请参阅:
- How does x86 paging work?(页面-table 格式的内部结构,以及 OS 如何管理它和 CPU 读取它)。
- What is the state of the art in Memory Protection?
but actually put 2 bytes (charcode for 1 and newline symbol) into the memory location.
mov [val], eax
是一个 4 字节的存储。操作数大小由寄存器决定。如果您想进行 2 字节存储,请使用 mov [val], ax
.
有趣的事实:MASM 会就操作数大小不匹配发出警告或错误,因为它根据在它们之后保留 space 的声明神奇地将大小与符号名称相关联。 NASM 不会妨碍你,所以如果你写 mov [val], 0x0A31
,那将是一个错误。这两个操作数都没有暗示大小,因此您需要 mov dword [val], 0x0A31
(或 word
或 byte
)。
将 val
放在页面末尾会出现段错误
出于某种原因,BSS 不从 32 位二进制文件的页首开始,而是接近页首。您没有链接到会占用 BSS 中大部分页面的任何其他内容。 nm bss-no-segfault
显示它在 0x080490a8
,一个 4k 页面是 0x1000
字节,所以 BSS 映射中的最后一个字节将是 0x08049fff
.
当我在.text
部分添加一条指令时,BSS起始地址似乎发生了变化,所以推测链接器在这里的选择与将东西打包到ELF executable中有关。意义不大,因为BSS并没有存储在文件中,它只是一个基地址+长度。我不会去那个兔子洞;我确定有一个原因使 .text
稍微大一点会导致从页面开头开始的 BSS,但我不知道它是什么。
无论如何,如果我们构建 BSS 使得 val
恰好在页面结束之前,我们会得到一个错误:
... same .text
section .bss
dummy: resb 4096 - 0xa8 - 2
val: resb 1
;; could have done this instead of making up constants
;; ALIGN 4096
;; dummy2: resb 4094
;; val2: resb
然后构建 运行:
$ asm-link -m32 bss-no-segfault.asm
+ yasm -felf32 -Worphan-labels -gdwarf2 bss-no-segfault.asm
+ ld -melf_i386 -o bss-no-segfault bss-no-segfault.o
peter@volta:~/src/SO$ nm bss-no-segfault
080490a7 B __bss_start
080490a8 b dummy
080490a7 B _edata
0804a000 B _end <--------- End of the BSS
08048080 T _start
08049ffe b val <--------- Address of val
gdb ./bss-no-segfault
(gdb) b _start
(gdb) r
(gdb) set disassembly-flavor intel
(gdb) layout reg
(gdb) p &val
= (<data variable, no debug info> *) 0x8049ffe
(gdb) si # and press return to repeat a couple times
mov [var], eax
段错误,因为它进入了未映射的页面。 mov [var], ax
会起作用(因为我在页面末尾之前放置了 var
2 个字节)。
此时,/proc/<PID>/smaps
显示:
... the r-x private mapping for .text
08049000-0804a000 rwxp 00000000 00:15 2885598 /home/peter/src/SO/bss-no-segfault
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Anonymous: 4 kB
...
[vvar] and [vdso] pages exported by the kernel for fast gettimeofday / getpid
关键事项:rwxp
表示 read/write/execute,并且是私有的。甚至在第一条指令之前就停止了,不知何故它已经 "dirty" (即写入)。文本段也是如此,但这是 gdb 将指令更改为 int3
.
所期望的
08049000-0804a000(和 4 kB
映射大小)向我们表明 BSS 仅映射了 1 页。没有数据段,只有文本和 BSS。
我正在试验汇编语言并编写了一个将 2 个硬编码字节打印到标准输出的程序。这是:
section .text
global _start
_start:
mov eax, 0x0A31
mov [val], eax
mov eax, 4
mov ebx, 1
mov ecx, val
mov edx, 2
int 0x80
mov eax, 1
int 0x80
segment .bss
val resb 1; <------ Here
请注意,我在 bss 段内只保留了 1 个字节,但实际上将 2 个字节(1
和 newline
符号的字符码)放入内存位置。该程序运行良好。它打印了 1
个字符,然后是 newline
。
但我预计会出现分段错误。为什么没有发生。我们只保留了1个字节,却放了2个。
x86,像大多数其他现代架构一样,使用 paging / virtual memory for memory protection. 在 x86 上(再次像许多其他架构),粒度是 4kiB。
存储到 val
的 4 字节不会出错,除非链接器恰好将它放在页面的最后 3 个字节中,并且下一页未映射。
实际发生的是,您只是覆盖了 val
之后的内容。在这种情况下,它只是未使用 space 到页面末尾。如果您在 BSS 中有其他静态存储位置,您会踩到它们的值。 (如果你愿意,可以称它们为 "variables",但是 "variable" 的高级概念并不仅仅意味着内存位置,变量可以存在于寄存器中并且永远不需要地址。 )
除了上面链接的维基百科文章外,另请参阅:
- How does x86 paging work?(页面-table 格式的内部结构,以及 OS 如何管理它和 CPU 读取它)。
- What is the state of the art in Memory Protection?
but actually put 2 bytes (charcode for 1 and newline symbol) into the memory location.
mov [val], eax
是一个 4 字节的存储。操作数大小由寄存器决定。如果您想进行 2 字节存储,请使用 mov [val], ax
.
有趣的事实:MASM 会就操作数大小不匹配发出警告或错误,因为它根据在它们之后保留 space 的声明神奇地将大小与符号名称相关联。 NASM 不会妨碍你,所以如果你写 mov [val], 0x0A31
,那将是一个错误。这两个操作数都没有暗示大小,因此您需要 mov dword [val], 0x0A31
(或 word
或 byte
)。
将 val
放在页面末尾会出现段错误
出于某种原因,BSS 不从 32 位二进制文件的页首开始,而是接近页首。您没有链接到会占用 BSS 中大部分页面的任何其他内容。 nm bss-no-segfault
显示它在 0x080490a8
,一个 4k 页面是 0x1000
字节,所以 BSS 映射中的最后一个字节将是 0x08049fff
.
当我在.text
部分添加一条指令时,BSS起始地址似乎发生了变化,所以推测链接器在这里的选择与将东西打包到ELF executable中有关。意义不大,因为BSS并没有存储在文件中,它只是一个基地址+长度。我不会去那个兔子洞;我确定有一个原因使 .text
稍微大一点会导致从页面开头开始的 BSS,但我不知道它是什么。
无论如何,如果我们构建 BSS 使得 val
恰好在页面结束之前,我们会得到一个错误:
... same .text
section .bss
dummy: resb 4096 - 0xa8 - 2
val: resb 1
;; could have done this instead of making up constants
;; ALIGN 4096
;; dummy2: resb 4094
;; val2: resb
然后构建 运行:
$ asm-link -m32 bss-no-segfault.asm
+ yasm -felf32 -Worphan-labels -gdwarf2 bss-no-segfault.asm
+ ld -melf_i386 -o bss-no-segfault bss-no-segfault.o
peter@volta:~/src/SO$ nm bss-no-segfault
080490a7 B __bss_start
080490a8 b dummy
080490a7 B _edata
0804a000 B _end <--------- End of the BSS
08048080 T _start
08049ffe b val <--------- Address of val
gdb ./bss-no-segfault
(gdb) b _start
(gdb) r
(gdb) set disassembly-flavor intel
(gdb) layout reg
(gdb) p &val
= (<data variable, no debug info> *) 0x8049ffe
(gdb) si # and press return to repeat a couple times
mov [var], eax
段错误,因为它进入了未映射的页面。 mov [var], ax
会起作用(因为我在页面末尾之前放置了 var
2 个字节)。
此时,/proc/<PID>/smaps
显示:
... the r-x private mapping for .text
08049000-0804a000 rwxp 00000000 00:15 2885598 /home/peter/src/SO/bss-no-segfault
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Anonymous: 4 kB
...
[vvar] and [vdso] pages exported by the kernel for fast gettimeofday / getpid
关键事项:rwxp
表示 read/write/execute,并且是私有的。甚至在第一条指令之前就停止了,不知何故它已经 "dirty" (即写入)。文本段也是如此,但这是 gdb 将指令更改为 int3
.
08049000-0804a000(和 4 kB
映射大小)向我们表明 BSS 仅映射了 1 页。没有数据段,只有文本和 BSS。