为什么我必须玩“rsp”来调用 C++ 函数?

fei*_*ty2 3 windows assembly x86-64 masm calling-convention

我刚刚开始我的组装之旅,很明显我是一个新手,我一直在编写相当简单和基本的程序,我只是注意到一些奇怪的东西(对我来说)。

一个程序给出以二进制 111 结尾的表格中的数字计数

入口点:

#include <iostream>
#include <cstdlib>

extern "C" auto _start(void *, void *)->void;

auto print_msg(char *msg) {
    std::cout << msg;
}

auto print_int(uint64_t val) {
    std::cout << val;
}

auto main()->int {
    _start(print_int, print_msg);
    std::cout << std::endl;
    system("pause");
}
Run Code Online (Sandbox Code Playgroud)

集会:

.const
_tab    dw 65535, 61951, 61949, 61925, 61927, 61734, 61735, 61728
_LENGTH = ($ - _tab) / 2
_msg_1  db 'There are ', 0
_msg_2  db ' numbers ending with 111 in binary!', 0

.code
_start proc
         push     r15
         push     r14
         sub      rsp, 32 + 16
         mov      r14, rcx
         mov      r15, rdx
         xor      rcx, rcx
         xor      r9,  r9
         lea      r8,  _tab
_LOOP:   movzx    rax, word ptr [r8]
         and      rax, 111b
         cmp      rax, 111b
         jz       _INC
         jmp      _END_IF
_INC:    inc      rcx
_END_IF: inc      r9
         add      r8,  2
         cmp      r9,  _LENGTH
         jne      _LOOP
         mov      [rsp + 32], rcx
         lea      rcx, _msg_1
         call     r15
         mov      rcx, [rsp + 32]

         sub      rsp, 8
         call     r14
         add      rsp, 8

         lea      rcx, _msg_2
         call     r15
         add      rsp, 32 + 16
         pop      r14
         pop      r15
         ret
_start endp

end
Run Code Online (Sandbox Code Playgroud)

如果我在“call r14”周围注释“sub rsp, 8”和“add rsp, 8”,程序将立即崩溃,这对我来说没有意义,我想知道它为什么会发生,而且,如果我用“push rcx”和“pop rcx”替换了“mov [rsp + 32], rcx”和“mov rcx,[rsp + 32]”,输出将是垃圾,我也很好奇

Pet*_*des 5

视窗64位调用约定要求RSP的16B对准CALL指令之前(但因此保证 rsp%16 == 8在函数入口,后call推返回地址)。这解释了sub rsp,8围绕函数调用。

它还需要为被调用函数的使用保留32B 的阴影空间(又名home 空间),这就是它sub rsp, 32 + 16正在做的事情。


将它们组合在一起并sub rsp, 32 + 16 + 8在函数输入时将它们组合在一起是明智的,然后在结尾之前不要弄乱 RSP。(在执行奇数个pushes的函数中,+8负责重新对齐堆栈。)

[rsp+32]高字节不会被 a 踩到call,而低字节则不然。

被调用的函数可以自由使用其返回地址上方的那 32 个字节。这解释了为什么如果你只是在 CALL 周围推送/弹出你会得到乱码的输出,因为你的数据将在阴影空间中。


有关ABI/调用约定链接,请参阅标记 wiki。

  • @clockw0rk:我不同意。x86-64 函数调用通常会避免堆栈参数,因此 RSP 不会不断移动,因此人们通常仍然可以轻松跟踪相对于 RSP 的堆栈位置。当您可以只使用 RSP 时,无需引入设置帧指针的低效率,而将 RBP 留作其他用途。(这也是需要阅读的额外代码)。另外,这是 64 位代码,因此显然 `push ebp` 不可编码,但我假设您的意思是 `push rbp` 等等。 (2认同)