我正在尝试汇编语言,并编写了一个程序,可以将2个硬编码字节打印到stdout中。这是:
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
符号的charcode)。程序运行良好。它先打印1
字符,然后打印newline
字符。
但我预料到了分割错误。为什么没有发生。我们只保留了1个字节,但放了2个。
x86与大多数其他现代体系结构一样,使用分页/虚拟内存来保护内存。在x86上(和许多其他体系结构一样),粒度为4kiB。
val
的4字节存储不会出错,除非链接器碰巧将其放置在页面的最后3个字节中,并且下一个页面未映射。
实际发生的情况是,您只需覆盖val
之后的内容。在这种情况下,它只是页面末尾未使用的空间。如果您在BSS中有其他静态存储位置,那么您可以根据它们的值进行调整。(如果你愿意,可以称之为"变量",但"变量"的高级概念不仅仅意味着内存位置,变量可以存在于寄存器中,而且不需要有地址。)
除了上面链接的维基百科文章,还请参阅:
- x86分页是如何工作的?(页表格式的内部,以及操作系统如何管理它和CPU如何读取它)
- 记忆保护的最新技术是什么
- 在x86和x64上,读取超过同一页中缓冲区末尾的内容是否安全
- 关于Linux中程序的内存布局
,但实际将2个字节(1和换行符的charcode)放入内存位置。
mov [val], eax
是一个4字节的存储。操作数大小由寄存器决定。如果要进行2字节存储,请使用mov [val], ax
。
有趣的事实是:MASM会警告或错误操作数大小不匹配,因为它会根据在符号名称后面保留空间的声明神奇地将大小与符号名称关联起来。NASM不会妨碍您,所以如果您编写mov [val], 0x0A31
,那将是一个错误。两个操作数都不包含大小,因此需要mov dword [val], 0x0A31
(或word
或byte
)。
将val
放置在页面末尾以获得segfault
由于某些原因,BSS不会在32位二进制文件中的页面开头开始,但它接近页面的开头。您没有链接到任何会占用BSS中大部分页面的其他内容。nm bss-no-segfault
显示它在0x080490a8
,4k页面是0x1000
字节,因此BSS映射中的最后一个字节将是0x08049fff
。
当我将指令添加到.text
部分时,BSS的起始地址似乎会发生变化,所以这里链接器的选择可能与将内容打包到ELF可执行文件中有关。这没有多大意义,因为BSS没有存储在文件中,它只是一个基地址+长度。我不会去兔子洞;我相信.text
稍微大一点会导致BSS从页面的开头开始,但IDK就是这样。
无论如何,如果我们构造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
$2 = (<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
表示读/写/执行,以及私有。即使在第一条指令之前停止,不知何故它已经"脏"了(即写入)。文本段也是如此,但这是gdb将指令更改为int3
时所期望的。
08049000-0804a000(和映射的4 kB
大小)向我们显示BSS只有1个页面被映射。没有数据段,只有文本和BSS。