记一次Bug 调试 —— 内存泄漏&&内存越界

前言

不得不说,这类Bug真的是特别难排查找,故障点往往远离Bug点,这就导致程序的运行非常的诡异和费解

不知道我理解的对不对,本文这样定义内存泄漏和内存溢出:

  • 内存泄漏
    指的是,没有释放申请的堆内存,如:
    int main(){
        void* p = malloc (100);
        //  free (p);
    }
    
    如果忘记free(p),就会导致内存的泄漏,内存泄漏并不会导致任何错误,但是会浪费内存。
  • 内存溢出
    指的是,访问的内存空间,超过了允许的范围,如:
    int main(){
        // 1、破坏栈空间
        int a[10];
        a[10] = 1;
        // 2、破坏堆空间
        int *b = (int *) malloc(10 * sizeof(int));
        b[10] = 1;
        // 3、访问非法地址
        int *p;
        *p = 1;
    }
    

工具

检查内存溢出和越界,需要一个强大的工具进行错误排查,这里选择的是Clion+valgrind,比如如下代码:

#include <malloc.h>
int main() {
    int *b = (int *) malloc(10 * sizeof(int));
    b[10] = 1;
}

将会得到这样的数据结果:

Clion的valgrind输出
有关valgrind的使用,可以参考valgrind 的使用.

源代码一

#include <pistache/http.h>
#include <proto/rfit.pb.h>

using namespace std;

struct FunctionRegisterEntry {
    FunctionRegisterEntry(RFIT_NS::FunctionRegisterMsg msg_,
                          Pistache::Async::Deferred<void> deferred_) :
            msg(std::move(msg_)),
            deferred(std::move(deferred_)) {}

    RFIT_NS::FunctionRegisterMsg msg;
    Pistache::Async::Deferred<void> deferred;
};

auto handle(Pistache::Polling::Epoll &poller, Pistache::PollableQueue<FunctionRegisterEntry> &queue) {
    return [&] {
        for (;;) {
            vector<Pistache::Polling::Event> events;
            int ready_fds = poller.poll(events);
            if (ready_fds == -1) return;
            for (auto e : events) {
                if (e.tag == queue.tag()) {
                    auto f = queue.popSafe();
                    f->deferred.resolve();
                    return;
                }
            }
        }
    };
}

void f(Pistache::PollableQueue<FunctionRegisterEntry> &queue) {
    RFIT_NS::FunctionRegisterMsg msg;
    string s = "mem leak";
    msg.set_dldata(s);
    auto p = Pistache::Async::Promise<void>(
            [&](Pistache::Async::Deferred<void> deferred) {
                FunctionRegisterEntry func(std::move(msg), std::move(deferred));
                queue.push(std::move(func));
            });
    p.then([&] {
        printf("%s", s.c_str());
    }, PrintException());
}

int main() {
    Pistache::Polling::Epoll poller;
    Pistache::PollableQueue<FunctionRegisterEntry> queue;
    queue.bind(poller);
    thread t(handle(poller, queue));
    f(queue);
    t.join();
}

源码其实比较复杂,但是这已经是尽力简化了,这里面使用了Pistache的两个类,分别是PollableQueue类Promise类,用于实现异步编程。此外还还使用了proto,创建了一个FunctionRegisterMsg类,其结构如下:

message FunctionRegisterMsg{
  string funcName = 1;

  uint64 memSize = 2;
  double coreRation = 3;
  uint32 concurrency = 4;

  bytes dlData = 5;
}
1、代码逻辑
  • main()函数创建了一个Poller和一个PollableQueue队列,并将队列绑定到Poller中;
  • 然后启动一个线程,线程的作用是启动Poller监控队列;
  • 然后main()函数,向队列中放入一个数据,子线程将异步的处理队列,主要就是将数据从队列中取出,不做任何处理,然后执行回调函数,打印“mem leak”
2、valgrind执行结果

内存越界

上面的结果中由于内存越界导致的异常提醒有三处,因为越界会导致各种不可预测的结果,因此异常信息可能很多很杂乱。在这里,他们都指向了printf()函数,说明是在调用printf()时,出现了bug。

首先简化一下f()的实现:

void f() {
    string s = "mem leak";
    auto p = Pistache::Async::Promise<void>([&](Pistache::Async::Deferred<void> deferred) { ... });
    p.then(
       [&]{
           printf("%s", s.c_str());
    }, PrintException());
}

printf()函数打印的是string s,但是string s在整个流程中都没有被修改,为什么会出现异常呢?

这其实是由于异步机制导致的,因为虽然f()是由主线程调用的,但是p.then()注册的回调函数并不一定由谁来执行,这一点在Promise类中有详细的分析。

在这里,显然,当主线程执行then()时,异步处理还没有执行deferred.resolve(),那么f()将回调函数注册到Promise就返回了,当子线程执行完异步过程,调用deferred.resolve()时,会执行回调函数,也就是printf(),但是要打印的字符串s是引用的f()的局部变量string s = "mem leak";,其在f()返回后就被释放了,因此在这里等价于执行:

int main() {
    auto *s = new string("mem leak");
    delete s;
    printf("%s", s->c_str());
    return 0;
}

访问了已经被释放的内容,将会引起内存越界!
这个Bug我找了2天(),因为当时传入的是Respone对象,与一个简单的字符串相比,其引起的越界访问将产生大量错误,导致valgrind的结果非常的多,而且是一层套一层,很难定位到是哪个位置出现了错误!

3、bug修复

这个代码修复其实很容易,只要改一下then()的回调函数中的引用捕获改为值捕获即可:

p.then([&] {
        printf("%s", s.c_str());
    }, PrintException());

改为:
p.then([=] {
        printf("%s", s.c_str());
    }, PrintException());

此时,异常信息将只剩下一个

内存泄漏

这个bug我同样找了两天,非常的痛苦,最终发现,这个bug是Pistache的Queue的设计缺陷导致的。但是现在这个代码,太难分析的,需要进一步简化:

源代码二

int childIndex;

struct Child {
    Child() {
        s = static_cast<char *>(malloc(100));
        printf("Child() %d %p\n", index, s);
    };

    Child(Child &other) {
        s = static_cast<char *>(malloc(100));
        memcpy(s, other.s, 100);
        printf("Child(Child &other) %d %p\n", index, s);
    }

    Child(Child &&other) noexcept {
//        s=other.s;
//        other.s= nullptr;
        s = static_cast<char *>(malloc(100));
        memcpy(s, other.s, 100);
        printf("Child(Child &&other) %d %p\n", index, s);
    }

    ~Child() {
        free(s);
        printf("~Child() %d %p\n", index, s);
    }

    char *s;
    int index = childIndex++;
};

int main() {
    Child c;
    Queue<Child> q;
    q.push(c);
}
1、代码逻辑

定义了一个Child类以及构造函数、复制构造函数和移动构造函数。
需要注意的是,移动构造函数的实现是存在缺陷的,合理的实现应该是如注释所写的那样,正是这个缺陷最后导致了Pistache的Queue设计出现了内存泄漏问题。

2、执行结果

这段代码的执行结果是这样的:

Child() 0 0x55f6180
Child(Child &other) 1 0x55f6730
~Queue
Child(Child &&other) 2 0x55f67e0
~Child() 2 0x55f67e0
~Child() 0 0x55f6180

从结果上我们可以发现,代码缺少了一次析构函数的执行。
查看valgrind打印的信息,发现有100字节没有释放

valgrind信息
正如上面说的,如果使用合理的移动构造函数,那么就不会存在这个问题。但是Queue的设计应该考虑到这种情况,因为只要确保所有的对象都被释放,那么就不会出现内存泄漏,因此锅应该还由Pistache背上。
3、Queue源码分析
Pistache Queue是一种无锁的MPSC设计,即多生产者单消费者模型,算法并不难理解,就不展开了,我们主要来研究数据在push和pop过程中是怎么构造和释放的。

push

q.push(c);

 template <typename U>
        void push(U&& u)
        {
            Entry* entry = new Entry(std::forward<U>(u));
            auto* prev = head.exchange(entry);
            prev->next = entry;
        }

 template <class U>
            explicit Entry(U&& u)
                : storage()
                , next(nullptr)
            {
                new (&storage) T(std::forward<U>(u));
            }
  • push函数会首先new一个Entry对象,而在Entry的构造函数中,执行了new (&storage) T(std::forward<U>(u));因为我们传入的是c,而非std::move(c),因此将使用Child的复制构造函数创建一个新的Child对象,并将其值赋值给storage,此过程对应了输出结果中的Child(Child &other) 1 0x55f6730;因为Entry对象对象是通过new创建的,因此,storage是在堆中的,进而我们的c1也是在堆中,c1.s指向的数据块当然也是在堆中,因为是在复制构造函数中用malloc创建的。
virtual ~Queue()
        {
            printf("~Queue");
            while (!empty())
            {
                Entry* e = pop();
                e->data().~T();
                delete e;
            }
            delete tail;
        }
  • 然后程序结束,会调用queue的析构函数,因此会打印~Queue;
virtual Entry* pop()
        {
            auto* res  = tail;
            auto* next = res->next.load(std::memory_order_acquire);
            if (next)
            {
                tail = next;
                new (&res->storage) T(std::move(next->data()));
                return res;
            }
            return nullptr;
        }
  • 在析构函数中,会调用pop函数,pop函数我们需要关注的仅仅是new (&res->storage) T(std::move(next->data()));在这里next->data()就是我们在push阶段的storage,在这里我们可以看到使用了移动构造函数,将数据传递给了新的entry对象,如果在这里我们使用的是合理的构造函数,那么原来的c1的数据,将全部交给c2,但是在这里并没有,因此c1.s依然存在与堆中
  • 最后在~Queue中执行了e->data().~T(); delete e;分别用于释放c2和entry对象,注意entry对象并不会主动的释放c2!
    这个过程可能非常抽象和难以理解,这里用内存示意图来描述整个过程:






最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容