转载部分

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

2021-07-21  本文已影响0人  蟹蟹宁

前言

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

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

工具

检查内存溢出和越界,需要一个强大的工具进行错误排查,这里选择的是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、代码逻辑
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));
            }
virtual ~Queue()
        {
            printf("~Queue");
            while (!empty())
            {
                Entry* e = pop();
                e->data().~T();
                delete e;
            }
            delete tail;
        }
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;
        }
上一篇 下一篇

猜你喜欢

热点阅读