从汇编代码分析可变参数原理

C语言可变参数经常会使用到,比如在写回调函数时,有时候会使用不定参数的函数。另外经常使用的linux printf、sprintf都是不定参数的函数。但是函数是如何解析到这些不定参数的呢?有什么手段可以实现呢?如果我们把这些不定参数都统一放在某个能找到的地方,不就可以根据参数占用的内存大小,找到并一一解析出来。我们都知道函数调用过程中有两大法宝:函数栈和寄存器。实际上不定参数函数解析参数的时候也是使用这两大法宝。

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

double average(int num, ...)
{
    double res = 0;
    va_list args;
    va_start(args, num);
    for (int i = 0; i < num; ++i) {
        int arg = va_arg(args, int); 
        res += arg;
    }
    va_end(args);
    return res * 1.0 / num;
}

int main()
{
    double res = 0;
    res = average(10, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
    printf("%f\n", res);
    return 0;
}

查看main函数的反汇编代码:

int main()
{
...
    12f5:    6a 0b                    pushq  $0xb
    12f7:    6a 0a                    pushq  $0xa
    12f9:    6a 09                    pushq  $0x9
    12fb:    6a 08                    pushq  $0x8
    12fd:    6a 07                    pushq  $0x7
    12ff:    41 b9 06 00 00 00        mov    $0x6,%r9d
    1305:    41 b8 05 00 00 00        mov    $0x5,%r8d
    130b:    b9 04 00 00 00           mov    $0x4,%ecx
    1310:    ba 03 00 00 00           mov    $0x3,%edx
    1315:    be 02 00 00 00           mov    $0x2,%esi
    131a:    bf 0a 00 00 00           mov    $0xa,%edi
    131f:    b8 00 00 00 00           mov    $0x0,%eax
    1324:    e8 5a fe ff ff           callq  1183 <average>
...
...
}

这里只展示了部分相关的汇编代码。可以看到在调用average函数前,会先把函数入参放入栈和寄存器中。函数入参是函数参数列表从右到左依次放入栈中,剩余的6个放入寄存器中(如果没有超过六个则都存放在寄存器中)。

image
左边的参数表示放在栈中,右边的参数放在寄存器中。

1324:	e8 5a fe ff ff       	callq  1183 <average>

调用average函数,call指令会把下调指令存放到栈中,在average函数返回时,会把指令地址从栈中弹出,赋值给pc指针,继续执行。average函数的汇编代码分成下面几个部分:

1 把存放在寄存器的参数放入栈中

double average(int num, ...)
{
    1183:	f3 0f 1e fa          	endbr64 
    1187:	55                   	push   %rbp
    1188:	48 89 e5             	mov    %rsp,%rbp
    118b:	48 81 ec f0 00 00 00 	sub    $0xf0,%rsp
    1192:	89 bd 1c ff ff ff    	mov    %edi,-0xe4(%rbp)
    1198:	48 89 b5 58 ff ff ff 	mov    %rsi,-0xa8(%rbp)
    119f:	48 89 95 60 ff ff ff 	mov    %rdx,-0xa0(%rbp)
    11a6:	48 89 8d 68 ff ff ff 	mov    %rcx,-0x98(%rbp)
    11ad:	4c 89 85 70 ff ff ff 	mov    %r8,-0x90(%rbp)
    11b4:	4c 89 8d 78 ff ff ff 	mov    %r9,-0x88(%rbp)
...
}

经过上面的操作函数栈变化如下:

image

在把函数参数入栈的时候使用的是mov指令,因此rbp的值不会改变

2 取参

取参分为两个部分rbp下面部分的参数和rbp上面部分的参数。此时打印rbp的值为:

(gdb) p /x $rbp
$35 = 0x7fffffffdaa0

取rbp下面部分参数的汇编代码:

    1236:	8b 85 30 ff ff ff    	mov    -0xd0(%rbp),%eax
    123c:	83 f8 2f             	cmp    $0x2f,%eax
    123f:	77 23                	ja     1264 <average+0xe1>
    1241:	48 8b 85 40 ff ff ff 	mov    -0xc0(%rbp),%rax
    1248:	8b 95 30 ff ff ff    	mov    -0xd0(%rbp),%edx
    124e:	89 d2                	mov    %edx,%edx
    1250:	48 01 d0             	add    %rdx,%rax
    1253:	8b 95 30 ff ff ff    	mov    -0xd0(%rbp),%edx
    1259:	83 c2 08             	add    $0x8,%edx
    125c:	89 95 30 ff ff ff    	mov    %edx,-0xd0(%rbp)

mov -0xc0(%rbp),%rax是把-0xc0(%rbp)存放内容放到rax寄存器中。-0xc0(%rbp)放的是下面参数的基地址。add %rdx,%rax是把基地址加上偏移计算出实际存放的地址。

-0xd0(%rbp)出存放的是每次取参的offset,因为这里每个参数存在8个字节长度的栈上,所以看到add $0x8,%edx,然后再存放会-0xd0(%rbp)中。

取rbp上面部分参数的汇编代码:

    1264:	48 8b 85 38 ff ff ff 	mov    -0xc8(%rbp),%rax
    126b:	48 8d 50 08          	lea    0x8(%rax),%rdx
    126f:	48 89 95 38 ff ff ff 	mov    %rdx,-0xc8(%rbp)

-0xc8(%rbp)是存放上部分参数的基地址。mov -0xc8(%rbp),%rax是把基地址放到rax寄存器中。lea 0x8(%rax),%rdx基地址加偏移(大小为0x8)计算等到实际地址。mov %rdx,-0xc8(%rbp)保存该地址,便于下次计算代取参数的实际地址。下面这个图片展示的是栈rbp下面的存放的内容
image

posted @ 2025-07-10 16:20  cockpunctual  阅读(20)  评论(0)    收藏  举报