在函数内部使用 DB(定义字节)时出现分段错误

Luc*_*cas 3 assembly x86-64 nasm machine-code

我很好奇如果我把db 0x41这个.text部分放在它通常所属的地方,而不是放在.data它通常所属的地方,会发生什么。它会出现段错误,但到底为什么呢?

下面的代码是在 Mint 19.1 中使用 和 进行编译、链接和执行nasmld

无段错误:

global _start
section .data
db 0x41
section .text
_start:
    mov rax, 60    ; Exit(0) syscall
    xor rdi, rdi
    syscall
Run Code Online (Sandbox Code Playgroud)

段错误:

global _start
section .text
_start:
    db 0x41
    mov rax, 60     ; Exit(0) syscall
    xor rdi, rdi
    syscall
Run Code Online (Sandbox Code Playgroud)

我使用以下命令来组装、链接和运行它:

global _start
section .data
db 0x41
section .text
_start:
    mov rax, 60    ; Exit(0) syscall
    xor rdi, rdi
    syscall
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 7

如果您告诉汇编器在某处汇编任意字节,它就会这样做。 db是一个发出字节的伪指令,因此mov eax, 60db 0xb8, 0x3c, 0, 0, 0就 NASM 而言完全相同。任一者都会将这 5 个字节发送到当前位置的输出中。

如果您不希望数据被解码为指令(的一部分),请不要将其放在执行将到达的位置。 (例如,将其放在代码中section .rodata或之前或之后,就像问题中一样。如果您希望 asm 源在使用某些数据的每个函数附近定义静态/全局数据,则section .data可以在其他部分之间来回切换。)section .text


由于您使用的是 NASM 1,它会优化mov rax,60mov eax,60,因此该指令没有您期望从源中获得的 REX 前缀。

您手动编码的 REX 前缀将mov其更改为movR8D 而不是 EAX
41 b8 3c 00 00 00 mov r8d,0x3c

(我检查了objdump -drwC -Mintel而不是查找 REX 前缀中的哪个位。我只记得 REX.W 是0x48。但是0x41在 x86-64 中是 REX.B 前缀)。

因此,您的代码不会进行sys_exit系统调用,而是以 EAX=0运行syscall,即__NR_read. (Linux 内核在进程启动之前将除 RSP 之外的所有寄存器清零,并且在静态链接的可执行文件中,这_start是真正的入口点,没有首先运行动态链接器代码。因此 RAX 仍然为零)。

$ strace ./rex 
execve("./rex", ["./rex"], 0x7fffbbadad60 /* 54 vars */) = 0
read(0, NULL, 0)                        = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
+++ killed by SIGSEGV (core dumped) +++
Run Code Online (Sandbox Code Playgroud)

然后执行会进入后面 syscall的内容,在本例中是00 00解码为 的字节add [rax], al,因此会出现段错误。 如果您在 GDB 中运行代码,您就会看到这一点。


脚注 1:如果您使用的 YASM 未针对 32 位操作数大小进行优化

Intel 的手册称,一条指令上有 2 个 REX 前缀是非法的。我预计会出现非法指令错误(#UD 机器异常 -> 内核发出 SIGILL),但我的 Skylake CPU 忽略第一个 REX 前缀并将其解码为mov rax, sign_extended_imm32.

单步执行,它被视为一个长指令,所以我猜 Skylake 选择像其他多个前缀的情况一样处理它,其中只有最后一个类型有效。(但请记住,这并不是面向未来的,其他 x86 CPU 可能会以不同的方式处理它。)


其他情况下的相关/相同错误:

  • @ecm:同样,手册说 REX 必须是最后一个前缀。在SKL上,`41 66 b8 ff ff ff ff`完全忽略REX,并写入AX=-1,单步在`mov ax,-1`的imm16之后停止。尽管 REX.B=1,但它不会写入 R8W,就像使用有效顺序的前缀一样。我没有进行广泛的测试,当然,任何未来的 CPU 或现有的其他 CPU 的行为都与 Skylake 一样的保证为零。如果您想深入挖掘,关于具有无效前缀组合的真实 CPU 的行为的单独问答将是发布结果的好地方。 (2认同)
  • @user13947194:如果需要,您*可以*在函数中间使用 `section .rodata` / `.local_string: db "hi",0` / `section .text` ,并将机器代码放入 `.text 中` 但全局常量或变量进入另一个部分。标签“范围”不关心切换部分,因此您仍然可以使用点本地标签。如果将函数的数据放在它后面而不是中间,那么标签范围也有效。如果您喜欢混合内容,您甚至可以制作一个宏来切换各部分并返回一行。 (2认同)