调用但不跳转到 rax 中的地址时出现段错误
Segfault when calling, but not jumping to, address in rax
我正在使用类似汇编程序的 API(它不是 真正的 汇编程序,但它可以发出机器代码),我正在调试和玩弄它.它专门用于 System V x86_64 ABI,所以我只打算讨论 SysV 调用约定等。
出于某种原因,当我出于测试目的发出这样的人为代码时
builder.emit_sub(rsp, 1);
builder.emit_movq_vr(reinterpret_cast<uint64_t>(&hello_world), rax);
builder.emit_call(rax);
builder.emit_add(rsp, 1);
builder.emit_ret();
调用时发生段错误(运行时,不是汇编时),但
builder.emit_movq_vr(reinterpret_cast<uint64_t>(&hello_world), rax);
builder.emit_jmp(rax);
成功就好了。失败点似乎在 call
指令处,但我不知道伪汇编程序出了什么问题。它可能会发出错误的操作码操作数或其他东西,但我不确定。原始发出的机器代码对于错误代码看起来像这样,以及它应该表示的操作码,如一些简单的调试语句所打印
sub 48 81 EC 01 00 00 00
movqvr 48 B8 63 80 AA 01 01 00 00 00
call FF D0
add 48 81 C4 01 00 00 00
ret C3
备注:movqvr
不是真正的指令[助记符];最后的 vr
对我来说只是一个调试注释,它是一种 "move imm64 to reg" 指令。
备注:sub
和 add
是将堆栈对齐到 16 字节边界,我认为这在这个 ABI 中是必需的。它们本来可以更好地写成 push rax
和 pop rax
(如果 return 值需要 rax
,则写成 pop rcx
),但请忽略这一点,除非是这个弄乱了调用(例如,如果 rsp
没有被正确修改)。
是的,在 System V ABI 中,堆栈在每个 call
指令之前 与 16 字节边界对齐。因此,在函数入口处,它需要另一个 8 字节(不是 1)才能到达下一个 16 字节边界。请记住,在 C 中,指针差异按 sizeof(type)
缩放,但在 asm 中它们不是。
是的,push rax
/ pop rcx
将是一个不错的选择,如果 clang / LLVM 不需要推送奇数个调用保留寄存器或保留任何额外的堆栈 space。如果您确实需要为本地人保留任何堆栈 space,请使用将使 rsp
16 字节对齐的偏移量。
顺便说一句,当立即数适合符号扩展的 8 位值(即 if ((int8_t)imm == imm)
)时,您可以通过使用 sub r/m64, imm8
编码来节省代码大小。此外,如果您需要加/减 +128,请注意 -128
适合 imm8,因此您可以 add rsp, -128
(例如,在奇数个 push
指令之后)。
如果您知道您的代码 运行 来自的地址,您应该使用 call rel32
编码,而不是寄存器间接调用。但是你是对的,跳转到任意 64 位地址需要这个 mov r64, imm64
序列,而不是直接的 call
.
您是否使用调试器找出 hello_world
崩溃的位置?也许如果它调用 printf
(而不是 puts
),它忘记将 al
(使用 xor eax,eax
)归零以指示 XMM 寄存器中没有 FP args,所以 printf 可能使用了一些16 字节 SSE 对齐要求存储到堆栈?
RSP 甚至没有 qword 对齐是非常糟糕的,但我不希望它崩溃任何会崩溃的东西 8 字节对齐(但不是 16)。
我正在使用类似汇编程序的 API(它不是 真正的 汇编程序,但它可以发出机器代码),我正在调试和玩弄它.它专门用于 System V x86_64 ABI,所以我只打算讨论 SysV 调用约定等。
出于某种原因,当我出于测试目的发出这样的人为代码时
builder.emit_sub(rsp, 1);
builder.emit_movq_vr(reinterpret_cast<uint64_t>(&hello_world), rax);
builder.emit_call(rax);
builder.emit_add(rsp, 1);
builder.emit_ret();
调用时发生段错误(运行时,不是汇编时),但
builder.emit_movq_vr(reinterpret_cast<uint64_t>(&hello_world), rax);
builder.emit_jmp(rax);
成功就好了。失败点似乎在 call
指令处,但我不知道伪汇编程序出了什么问题。它可能会发出错误的操作码操作数或其他东西,但我不确定。原始发出的机器代码对于错误代码看起来像这样,以及它应该表示的操作码,如一些简单的调试语句所打印
sub 48 81 EC 01 00 00 00
movqvr 48 B8 63 80 AA 01 01 00 00 00
call FF D0
add 48 81 C4 01 00 00 00
ret C3
备注:movqvr
不是真正的指令[助记符];最后的 vr
对我来说只是一个调试注释,它是一种 "move imm64 to reg" 指令。
备注:sub
和 add
是将堆栈对齐到 16 字节边界,我认为这在这个 ABI 中是必需的。它们本来可以更好地写成 push rax
和 pop rax
(如果 return 值需要 rax
,则写成 pop rcx
),但请忽略这一点,除非是这个弄乱了调用(例如,如果 rsp
没有被正确修改)。
是的,在 System V ABI 中,堆栈在每个 call
指令之前 与 16 字节边界对齐。因此,在函数入口处,它需要另一个 8 字节(不是 1)才能到达下一个 16 字节边界。请记住,在 C 中,指针差异按 sizeof(type)
缩放,但在 asm 中它们不是。
是的,push rax
/ pop rcx
将是一个不错的选择,如果 clang / LLVM 不需要推送奇数个调用保留寄存器或保留任何额外的堆栈 space。如果您确实需要为本地人保留任何堆栈 space,请使用将使 rsp
16 字节对齐的偏移量。
顺便说一句,当立即数适合符号扩展的 8 位值(即 if ((int8_t)imm == imm)
)时,您可以通过使用 sub r/m64, imm8
编码来节省代码大小。此外,如果您需要加/减 +128,请注意 -128
适合 imm8,因此您可以 add rsp, -128
(例如,在奇数个 push
指令之后)。
如果您知道您的代码 运行 来自的地址,您应该使用 call rel32
编码,而不是寄存器间接调用。但是你是对的,跳转到任意 64 位地址需要这个 mov r64, imm64
序列,而不是直接的 call
.
您是否使用调试器找出 hello_world
崩溃的位置?也许如果它调用 printf
(而不是 puts
),它忘记将 al
(使用 xor eax,eax
)归零以指示 XMM 寄存器中没有 FP args,所以 printf 可能使用了一些16 字节 SSE 对齐要求存储到堆栈?
RSP 甚至没有 qword 对齐是非常糟糕的,但我不希望它崩溃任何会崩溃的东西 8 字节对齐(但不是 16)。