C中va_list在32位和64位机器的区别与差异

在将程序从32位机器移植到64位机器的过程中经常出现一些奇奇怪怪的错误,这里记录一下在使用可变参数的过程中导致在32位机器上正常运行的程序移植到64位机器上之后出现段错误的发现过程以及解决方案。

首先看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void parse(va_list ap) {
char* arg;
arg = va_arg(ap, char*);
std::cout << arg << std::endl
<< strlen(arg) << std::endl;
}
void test(const char* format, ...) {
va_list ap;
va_start(ap, format);
for (int i = 0; i < 2; i++) {
parse(ap);
}
va_end(ap);
}
int main() {
test("hget %s %s", "abc", "123456");
}

32位机器的运行结果如下:

1
2
3
4
abc
3
abc
3

64位机器运行结果如下:

1
2
3
4
abc
3
123456
6

原因分析

出现上述结果的原因是由于va_list类型在32位和64位机器的类型不同导致的.

32位va_list

在32位上,va_list的定义为:

1
2
//注意,由于中间宏过多,这里省去了中间如_VA_LIST宏,直接给出实际定义。
typedef va_list char**;

64位va_list

在64位上va_list定义为一个结构体数组,并且数组中记录了可变参数被读的偏移量:

1
2
3
4
5
6
7
// Figure 3.34
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];

程序异常分析

当在32位机器上将va_list(char**)作为参数传递给函数的时候,该函数将从头开始读取该变长参数,还是使用va_list完毕并不记录当前va_list被读的偏移量,所以当第二次传入该va_list还是从头开始读取。

当在64为机器上将va_list(struct 数组)作为参数传递给函数的时候,该函数读取va_list完毕之后,将读取的偏移量记录在结构体中,由于其为数组传入函数,所以该被调用的函数改变了传入的va_list的偏移量。导致下次调用该函数从记录的偏移量开始读,造成不可预测或者内存越界等问题。

移植解决方案

将va_list初始化写到for循环内部,每次调用函数前都初始化va_list即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void parse(va_list ap) {
char* arg;
arg = va_arg(ap, char*);
std::cout << arg << std::endl
<< strlen(arg) << std::endl;
}
void test(const char* format, ...) {
for (int i = 0; i < 2; i++) {
va_list ap;
va_start(ap, format);
parse(ap);
va_end(ap);
}
}
int main() {
test("hget %s %s", "abc", "123456");
}

参考:
https://stackoverflow.com/questions/4958384/what-is-the-format-of-the-x86-64-va-list-structure
http://blog.csdn.net/doubleface999/article/details/55798710