仔細研究了下va_list的實現原理。通過看對應的彙編代碼實現和Intel ABI手冊,發現:
- 在x86平臺下,va_list可變傳參是通過棧來進行;
- 在x64平臺下,va_list可變傳參是是默認的calling convention;
這是Inter官方的Linux ABI文檔,詳細說明了可變參的底層實現,以及相關的使用約定。
The va_list type is an array containing a single element of one structure containing the necessary information to implement the va_arg macro.
The va_start macro initializes the structure as follows:
- reg_save_area The element points to the start of the register save area.
- overflow_arg_area This pointer is used to fetch arguments passed on the stack.
It is initialized with the address of the first argument passed on the stack, if
any, and then always updated to point to the start of the next argument on the stack.- gp_offset The element holds the offset in bytes from reg_save_area to the place where the next available general purpose argument register is saved.
In case all argument registers have been exhausted, it is set to the value 48 (6 ∗ 8).- fp_offset The element holds the offset in bytes from reg_save_area to the place where the next available floating point argument register is saved. In case all argument registers have been exhausted, it is set to the value 304 (6 ∗ 8 + 16 ∗ 16).
下面通過一個例子結合對應的彙編代碼詳細說明:
#include <stdio.h>
#include <stdarg.h>
#if __GNUC__
#if __x86_64__
#define ENVIRONMENT64
#else
#define ENVIRONMENT32
#endif
#endif
#if defined(ENVIRONMENT64)
long add(long num, ...)
{
long sum = 0, i, tmp;
/*
(gdb) pt va_list
type = struct __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} [1]
*/
va_list va;
printf("ENVIRONMENT64\n");
/*
(gdb) p va
$2 = {{
gp_offset = 8,
fp_offset = 48,
overflow_arg_area = 0x7fffffffe520,
reg_save_area = 0x7fffffffe460
}}
(gdb) x/6g va.reg_save_area // 這裏g表示一次打印8字節
0x7fffffffe460: 0x0000000000000001 0x0000000000000002
0x7fffffffe470: 0x0000000000000003 0x0000000000000004
0x7fffffffe480: 0x0000000000000005 0x0000000000000006
(gdb) x/2g va.overflow_arg_area
0x7fffffffe520: 0x0000000000000007 0x0000000000000008
*/
va_start(va, num);
for (i = 0; i < num; i++) {
tmp = va_arg(va, int);
sum += tmp;
printf("%ld ", tmp);
}
printf("\n");
va_end(va);
return sum;
}
#endif
#if defined(ENVIRONMENT32)
long add(long num, ...)
{
long sum = 0, i, tmp;
long *arg = &num + 1;
printf("ENVIRONMENT32\n");
for (i = 0; i < num; i++)
{
tmp = arg[i];
sum += tmp;
printf("%ld ", tmp);
}
printf("\n");
return sum;
}
#endif
int main()
{
/* ENVIRONMENT32, 使用棧來傳參
0x080484f5 <+9>: movl $0x8,0x1c(%esp)
0x080484fd <+17>: movl $0x7,0x18(%esp)
0x08048505 <+25>: movl $0x6,0x14(%esp)
0x0804850d <+33>: movl $0x5,0x10(%esp)
0x08048515 <+41>: movl $0x4,0xc(%esp)
0x0804851d <+49>: movl $0x3,0x8(%esp)
0x08048525 <+57>: movl $0x2,0x4(%esp)
0x0804852d <+65>: movl $0x7,(%esp)
0x08048534 <+72>: call 0x804847d <add>
*/
/* ENVIRONMENT64, 使用寄存器+棧(默認calling convention)
0x0000000000400711 <+8>: movl $0x8,0x8(%rsp)
0x0000000000400719 <+16>: movl $0x7,(%rsp)
0x0000000000400720 <+23>: mov $0x6,%r9d
0x0000000000400726 <+29>: mov $0x5,%r8d
0x000000000040072c <+35>: mov $0x4,%ecx
0x0000000000400731 <+40>: mov $0x3,%edx
0x0000000000400736 <+45>: mov $0x2,%esi
0x000000000040073b <+50>: mov $0x7,%edi
0x0000000000400740 <+55>: mov $0x0,%eax
0x0000000000400745 <+60>: callq 0x4005bd <add>
*/
long sum = add(7, 2, 3, 4, 5, 6, 7, 8);
printf("sum is %ld\n", sum);
return 0;
}
函數運行如下。如果用的 Ubuntu 64位需要安裝下32位的庫:
- sudo apt-get install gcc-multilib
- sudo apt-get install g++-multilib
root@ubuntu:/media/psf/Home/iLearn/learn_valist# gcc main.c
root@ubuntu:/media/psf/Home/iLearn/learn_valist# ./a.out
ENVIRONMENT64
2 3 4 5 6 7 8
sum is 35
root@ubuntu:/media/psf/Home/iLearn/learn_valist# gcc main.c -m32
root@ubuntu:/media/psf/Home/iLearn/learn_valist# ./a.out
ENVIRONMENT32
2 3 4 5 6 7 8
sum is 35