前言
昨天喝酒,今天一早上班就被同事叫过来一起看一个诡异的事情,其诡异程度一度让我怀疑自己还没酒醒。
问题是这样的:使用libcurl库,设置了CURLOPT_HEADERFUNCTION的回调,代码简化一下是这样的:
static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
Task *task = (Task *)userdata;
return task->receiveHeaders(buffer, nitems * size);
}
void Task::setup() {
// 其他处理
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, this);
// 其他处理
}
size_t Task::receiveHeaders(char *buffer, size_t bufferSize) {
int code = 0;
curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, &code);
if (code == 200) {
// ..
} else {
// ..
}
return bufferSize;
}
在Task::receiveHeaders 函数的 return bufferSize; 处打断点,查看传入的 bufferSize 参数,发现其值总是 0,但是,在查看调用栈的上一帧 header_callback 处 nitems * size 明显不应该是0。
面对如此诡异的问题,引起了我的极大兴趣。经过了多番调查研究,终于真相大白。
首次尝试
完全没有理由的怀疑 items * size是不是计算错误了?于是做了一个修改,直接把这两个参数原样传递给receiveHeaders,于是函数改成这样了:
receiveHeaders(char *buffer, size_t bufferSize, size_t size, size_t nitems) {
int code = 0;
curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, &code);
// ..
return bufferSize;
}
断点一看,好家伙,这次bufferSize的计算结果正确的,size的结果也跟传入参数一致,唯独是 nitems参数变成0了。
这个尝试得到一个结论:是最后一个参数莫名其妙变成了0.
第二次尝试
也是完全没有理由的怀疑是函数调用入栈的问题,于是做了一个修改,把receiveHeaders函数改成静态成员函数。由于静态成员函数不能访问成员变量,于是,顺手把所有业务代码都注释了,只返回 bufferSize.
于是函数改成了这样了:
receiveHeaders(char *buffer, size_t bufferSize, size_t size, size_t nitems) {
return bufferSize;
}
好家伙,问题消失了!最后一个参数正常了。 那么,问题看起来已经很清晰了,就是业务代码导致的问题。但是,业务代码到底是什么问题呢?业务代码完全没有修改参数值的地方。
第三次尝试
有了第二次尝试,误打误撞发现的业务代码问题之后,问题就好办了。函数恢复回成员函数定义,二分法注释部分代码逐一尝试。最终锁定就是开头的两行代码导致的:
int code = 0;
curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, &code);
这次不再头昏了,&code这里的一个输出参数应该很明确的表示,最后一个参数很可能就是被这里写掉了。于是,
查看一下libcurl中CURLINFO_RESPONSE_CODE的定义:
typedef enum {
CURLINFO_NONE, /* first, never use this */
CURLINFO_EFFECTIVE_URL = CURLINFO_STRING + 1,
CURLINFO_RESPONSE_CODE = CURLINFO_LONG + 2,
};
作者真是一句多余的话也没说,但是,从= CURLINFO_LONG + 2的定义方式,多少感觉到,这个getinfo的输出参数类型应该是个long而不应该是int。翻libcurl的线上手册,确认了这一点:
Example
CURL *curl = curl_easy_init();
if(curl) {
CURLcode res;
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
res = curl_easy_perform(curl);
if(res == CURLE_OK) {
long response_code;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
}
curl_easy_cleanup(curl);
}
最终尝试
把 int code = 0 改成 long code = 0,问题解决。
问题解析
为什么会这样呢?
这就要从函数的调用栈说起来:(网上随便搜的一个图,侵删)
调用receiveHeaders时,调用栈从高地址到低地址,依次如下顺序排列
| 变量 | 类型 | 字节数(按64位平台讲述) |
|---|---|---|
| 入参 this | intptr | 8 |
| 入参 buffer | char * | 8 |
| 入参 bufferSize | size_t | 8 |
| 入参 size | size_t | 8 |
| 入参 nitems | size_t | 8 |
| 局部变量 code | int | 4 |
由于局部变量code定义的类型是int占用的是4字节的大小。
但是,传递给curl_easy_getinfo后,函数是当成一个long来处理,写入了8字节的值,那么,其中高位的4个字节就自然的写到了入参nitems的低4个字节的内存位置了。这就是为什么总是最后一个参数变成0的缘故了。
为了更好的理解上述原理,可以观看一下如下测试程序的输出:
#include <iostream>
using namespace std;
size_t func(size_t a, size_t b) {
cout << &a << endl;
cout << &b << endl;
int c = 10;
cout << &c << endl;
long *pc = (long *)&c;
*pc = 30;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return a+b;
}
int main() {
func(1, 2);
return 0;
}
最后
这个世界没有鬼,有的只是对无知的恐惧。