程序集-将参数传递给函数调用



我目前正在通过分解C程序并试图理解它们的作用来练习汇编阅读。

我被一个琐碎的程序卡住了:一个简单的helloworld程序。

#include <stdio.h>
#include <stdlib.h>
int main() {
  printf("Hello, world!");
  return(0);
}

当我拆卸主:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000400526 <+0>: push   rbp
   0x0000000000400527 <+1>: mov    rbp,rsp
   0x000000000040052a <+4>: mov    edi,0x4005c4
   0x000000000040052f <+9>: mov    eax,0x0
   0x0000000000400534 <+14>:    call   0x400400 <printf@plt>
   0x0000000000400539 <+19>:    mov    eax,0x0  
   0x000000000040053e <+24>:    pop    rbp
   0x000000000040053f <+25>:    ret

我理解前两行:基指针保存在堆栈上(通过push-rbp,这会导致堆栈指针的值减少8,因为它已经"增长"了),堆栈指针的数值保存在基指针中(这样,参数和局部变量可以分别通过正偏移和负偏移轻松到达,而堆栈可以保持"增长")。

第三行提出了第一个问题:为什么0x4005c4("Hello,World!"字符串的地址)在edi寄存器中移动,而不是在堆栈上移动?printf函数不应该将该字符串的地址作为参数吗?据我所知,函数从堆栈中获取参数(但在这里,参数似乎放在寄存器中:edi)

在StackOverflow上的另一篇帖子中,我读到"printf@ptl"就像一个调用真正printf函数的存根函数。我试图反汇编这个函数,但它变得更令人困惑:

(gdb) disassemble printf
Dump of assembler code for function __printf:
   0x00007ffff7a637b0 <+0>: sub    rsp,0xd8
   0x00007ffff7a637b7 <+7>: test   al,al
   0x00007ffff7a637b9 <+9>: mov    QWORD PTR [rsp+0x28],rsi
   0x00007ffff7a637be <+14>:    mov    QWORD PTR [rsp+0x30],rdx
   0x00007ffff7a637c3 <+19>:    mov    QWORD PTR [rsp+0x38],rcx
   0x00007ffff7a637c8 <+24>:    mov    QWORD PTR [rsp+0x40],r8
   0x00007ffff7a637cd <+29>:    mov    QWORD PTR [rsp+0x48],r9
   0x00007ffff7a637d2 <+34>:    je     0x7ffff7a6380b <__printf+91>
   0x00007ffff7a637d4 <+36>:    movaps XMMWORD PTR [rsp+0x50],xmm0
   0x00007ffff7a637d9 <+41>:    movaps XMMWORD PTR [rsp+0x60],xmm1
   0x00007ffff7a637de <+46>:    movaps XMMWORD PTR [rsp+0x70],xmm2
   0x00007ffff7a637e3 <+51>:    movaps XMMWORD PTR [rsp+0x80],xmm3
   0x00007ffff7a637eb <+59>:    movaps XMMWORD PTR [rsp+0x90],xmm4
   0x00007ffff7a637f3 <+67>:    movaps XMMWORD PTR [rsp+0xa0],xmm5
   0x00007ffff7a637fb <+75>:    movaps XMMWORD PTR [rsp+0xb0],xmm6
   0x00007ffff7a63803 <+83>:    movaps XMMWORD PTR [rsp+0xc0],xmm7
   0x00007ffff7a6380b <+91>:    lea    rax,[rsp+0xe0]
   0x00007ffff7a63813 <+99>:    mov    rsi,rdi
   0x00007ffff7a63816 <+102>:   lea    rdx,[rsp+0x8]
   0x00007ffff7a6381b <+107>:   mov    QWORD PTR [rsp+0x10],rax
   0x00007ffff7a63820 <+112>:   lea    rax,[rsp+0x20]
   0x00007ffff7a63825 <+117>:   mov    DWORD PTR [rsp+0x8],0x8
   0x00007ffff7a6382d <+125>:   mov    DWORD PTR [rsp+0xc],0x30
   0x00007ffff7a63835 <+133>:   mov    QWORD PTR [rsp+0x18],rax
   0x00007ffff7a6383a <+138>:   mov    rax,QWORD PTR [rip+0x36d70f]        # 0x7ffff7dd0f50
   0x00007ffff7a63841 <+145>:   mov    rdi,QWORD PTR [rax]
   0x00007ffff7a63844 <+148>:   call   0x7ffff7a5b130 <_IO_vfprintf_internal>
   0x00007ffff7a63849 <+153>:   add    rsp,0xd8
   0x00007ffff7a63850 <+160>:   ret    
End of assembler dump.

eax上的两个mov操作(mov-eax,0x0)也让我有点困扰,因为我在这里没有得到它们的作用(但我更关心我刚才描述的内容)。提前谢谢。

gcc的目标是x86-64 System V ABI,除Windows之外的所有x86-64系统都使用它(由于各种历史原因)。它的调用约定在返回堆栈之前传递寄存器中的前几个参数。(另请参阅维基百科关于这个呼叫约定的基本摘要。)

是的,这与旧的32位调用约定不同,后者对所有内容都使用堆栈。这是一件好事。有关ABI文档的更多链接,以及大量其他内容,请参阅x86标记wiki。

   0x0000000000400526: push   rbp
   0x0000000000400527: mov    rbp,rsp         # stack-frame boilerplate
   0x000000000040052a: mov    edi,0x4005c4    # first arg
   0x000000000040052f: mov    eax,0x0         # 0 FP args in vector registers
   0x0000000000400534: call   0x400400 <printf@plt>
   0x0000000000400539: mov    eax,0x0         # return 0.  If you'd compiled with optimization, this and the previous mov would be  xor eax,eax
   0x000000000040053e: pop    rbp             # clean up stack frame
   0x000000000040053f: ret

指向静态数据的指针适合32位,这就是为什么它可以使用mov edi, imm32而不是movabs rdi, imm64

浮点参数在SSE寄存器(xmm0-xmm7)中传递,甚至传递给var参数函数。al表示矢量寄存器中有多少FP参数。(请注意,C的类型提升规则意味着变元函数的float参数总是提升为double,这就是为什么printf没有任何float的格式说明符,只有doublelong double)。


printf@ptl就像一个调用实际printf函数的存根函数。

是的,没错。过程链接表条目从jmp开始到动态链接器例程,该例程解析符号并修改PLT中的代码,将其直接转换为指向libc的printf定义映射的地址的jmpprintf__printf的弱别名,这就是为什么gdb在您要求反汇编printf之后为该地址选择__printf标签的原因。

Dump of assembler code for function __printf:
   0x00007ffff7a637b0 <+0>: sub    rsp,0xd8               # reserve space
   0x00007ffff7a637b7 <+7>: test   al,al                  # check if there were any FP args
   0x00007ffff7a637b9 <+9>: mov    QWORD PTR [rsp+0x28],rsi  # store the integer arg-passing registers to local scratch space
   0x00007ffff7a637be <+14>:    mov    QWORD PTR [rsp+0x30],rdx
   0x00007ffff7a637c3 <+19>:    mov    QWORD PTR [rsp+0x38],rcx
   0x00007ffff7a637c8 <+24>:    mov    QWORD PTR [rsp+0x40],r8
   0x00007ffff7a637cd <+29>:    mov    QWORD PTR [rsp+0x48],r9
   0x00007ffff7a637d2 <+34>:    je     0x7ffff7a6380b <__printf+91>  # skip storing the FP arg-passing regs if there were no FP args
   0x00007ffff7a637d4 <+36>:    movaps XMMWORD PTR [rsp+0x50],xmm0
   0x00007ffff7a637d9 <+41>:    movaps XMMWORD PTR [rsp+0x60],xmm1
   0x00007ffff7a637de <+46>:    movaps XMMWORD PTR [rsp+0x70],xmm2
   0x00007ffff7a637e3 <+51>:    movaps XMMWORD PTR [rsp+0x80],xmm3
   0x00007ffff7a637eb <+59>:    movaps XMMWORD PTR [rsp+0x90],xmm4
   0x00007ffff7a637f3 <+67>:    movaps XMMWORD PTR [rsp+0xa0],xmm5
   0x00007ffff7a637fb <+75>:    movaps XMMWORD PTR [rsp+0xb0],xmm6
   0x00007ffff7a63803 <+83>:    movaps XMMWORD PTR [rsp+0xc0],xmm7
       branch_target_from_test_je:
   0x00007ffff7a6380b <+91>:    lea    rax,[rsp+0xe0]            # some more stuff

因此,printf的实现通过将所有传递arg的寄存器(除了第一个保存格式字符串的寄存器)存储到本地数组来保持var args处理的简单性。它可以让指针遍历它们,而不需要像开关一样的代码来提取正确的整数或FP arg。它仍然需要跟踪前5个整数和前8个FP参数,因为它们与调用方推送到堆栈上的其余参数不连续。

Windows 64位调用约定的影子空间通过为函数提供空间将其寄存器参数转储到与堆栈上已有参数相邻的堆栈来简化这一点,但这不值得在每次调用上浪费32字节的堆栈,IMO.(请参阅我的回答和对其他回答的评论:为什么Windows64使用与x86-64上所有其他操作系统不同的调用约定?)

printf没有什么琐碎的,它不是您尝试做的事情的第一选择,但事实证明它并不过于复杂。

更简单的东西:

extern unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int x )
{
    return(more_fun(x)+7);
}
0000000000000000 <fun>:
   0:   48 83 ec 08             sub    $0x8,%rsp
   4:   e8 00 00 00 00          callq  9 <fun+0x9>
   9:   48 83 c4 08             add    $0x8,%rsp
   d:   83 c0 07                add    $0x7,%eax
  10:   c3                      retq  

并且使用该堆叠。用于返回的eax。

现在使用指针

extern unsigned int more_fun ( unsigned int * );
unsigned int fun ( unsigned int x )
{
    return(more_fun(&x)+7);
}
0000000000000000 <fun>:
   0:   48 83 ec 18             sub    $0x18,%rsp
   4:   89 7c 24 0c             mov    %edi,0xc(%rsp)
   8:   48 8d 7c 24 0c          lea    0xc(%rsp),%rdi
   d:   e8 00 00 00 00          callq  12 <fun+0x12>
  12:   48 83 c4 18             add    $0x18,%rsp
  16:   83 c0 07                add    $0x7,%eax
  19:   c3                      retq   

就这样,edi在你的情况下使用了。

双指针

extern unsigned int more_fun ( unsigned int *, unsigned int * );
unsigned int fun ( unsigned int x, unsigned int y )
{
    return(more_fun(&x,&y)+7);
}
0000000000000000 <fun>:
   0:   48 83 ec 18             sub    $0x18,%rsp
   4:   89 7c 24 0c             mov    %edi,0xc(%rsp)
   8:   89 74 24 08             mov    %esi,0x8(%rsp)
   c:   48 8d 7c 24 0c          lea    0xc(%rsp),%rdi
  11:   48 8d 74 24 08          lea    0x8(%rsp),%rsi
  16:   e8 00 00 00 00          callq  1b <fun+0x1b>
  1b:   48 83 c4 18             add    $0x18,%rsp
  1f:   83 c0 07                add    $0x7,%eax
  22:   c3                      retq   

现在使用edi和esi。对我来说,这一切看起来都是呼叫惯例…

字符串

extern unsigned int more_fun ( const char * );
unsigned int fun ( void  )
{
    return(more_fun("Hello World")+7);
}
0000000000000000 <fun>:
   0:   48 83 ec 08             sub    $0x8,%rsp
   4:   bf 00 00 00 00          mov    $0x0,%edi
   9:   e8 00 00 00 00          callq  e <fun+0xe>
   e:   48 83 c4 08             add    $0x8,%rsp
  12:   83 c0 07                add    $0x7,%eax
  15:   c3                      retq  

eax没有像printf中那样准备好,所以eax可能与后面的参数数量有关,试着在printf上添加更多的参数,看看eax是否发生了变化。

如果在命令行中添加-m32,则不使用edi。

00000000 <fun>:
   0:   83 ec 18                sub    $0x18,%esp
   3:   68 00 00 00 00          push   $0x0
   8:   e8 fc ff ff ff          call   9 <fun+0x9>
   d:   83 c4 1c                add    $0x1c,%esp
  10:   83 c0 07                add    $0x7,%eax
  13:   c3 

我怀疑推送是链接器的占位符,当链接器修补二进制文件时,它会将地址推送到字符串,这只是一个对象。所以我的猜测是,当你有一个64位指针时,前一两个指针进入寄存器,然后堆栈在寄存器用完后使用。

显然,编译器可以工作,因此这符合编译器调用约定。

extern unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int x )
{
    return(more_fun(x+5)+7);
}
0000000000000000 <fun>:
   0:   48 83 ec 08             sub    $0x8,%rsp
   4:   83 c7 05                add    $0x5,%edi
   7:   e8 00 00 00 00          callq  c <fun+0xc>
   c:   48 83 c4 08             add    $0x8,%rsp
  10:   83 c0 07                add    $0x7,%eax
  13:   c3                      retq   

根据Peter的评论进行更正。是的,这里确实使用了寄存器。

既然他提到了6个参数,让我们试试7个。

extern unsigned int more_fun
(
unsigned int,
unsigned int,
unsigned int,
unsigned int,
unsigned int,
unsigned int,
unsigned int
);
unsigned int fun (
unsigned int a,
unsigned int b,
unsigned int c,
unsigned int d,
unsigned int e,
unsigned int f,
unsigned int g
)
{
    return(more_fun(a+1,b+2,c+3,d+4,e+5,f+6,g+7)+17);
}
0000000000000000 <fun>:
   0:   48 83 ec 10             sub    $0x10,%rsp
   4:   83 c1 04                add    $0x4,%ecx
   7:   83 c2 03                add    $0x3,%edx
   a:   8b 44 24 18             mov    0x18(%rsp),%eax
   e:   83 c6 02                add    $0x2,%esi
  11:   83 c7 01                add    $0x1,%edi
  14:   41 83 c1 06             add    $0x6,%r9d
  18:   41 83 c0 05             add    $0x5,%r8d
  1c:   83 c0 07                add    $0x7,%eax
  1f:   50                      push   %rax
  20:   e8 00 00 00 00          callq  25 <fun+0x25>
  25:   48 83 c4 18             add    $0x18,%rsp
  29:   83 c0 11                add    $0x11,%eax
  2c:   c3                      retq   

毫无疑问,第7个参数是在调用之前从修改后的堆栈中提取并放回堆栈的。其他6个在寄存器中。

最新更新