为什么存储超过 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 个字节(1newline 符号的字符码)放入内存位置。该程序运行良好。它打印了 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(或 wordbyte)。


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。