昨天喝酒,今天一早上班就被同事叫过来一起看一个诡异的事情,其诡异程度一度让我怀疑自己还没酒醒。

问题是这样的:使用 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_callbacknitems * 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这里的一个输出参数应该很明确的表示,最后一个参数很可能就是被这里写掉了。于是, 查看一下libcurlCURLINFO_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位平台讲述)
入参 thisintptr8
入参 bufferchar *8
入参 bufferSizesize_t8
入参 sizesize_t8
入参 nitemssize_t8
局部变量 codeint4

由于局部变量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;

这个世界没有鬼,有的只是对无知的恐惧。

分类:
iOS