http://www.yebangyu.org/blog/2016/02/01/detectmemoryghostinmultithread/
多线程中的内存问题,一直被认为是噩梦般的存在,几乎只有高手、大仙才能解决。除了大量的打log、gdb调试、code review以及依靠多年的经验和直觉之外,有没有一些分析的手段和工具呢?答案是肯定的。本文首先介绍其中的一种:mprotect大法。通过mprotect,保护特定的感兴趣的内存,当有线程改写该区域时,会产生一个中断,我们在中断处理函数中把调用栈等信息打印出来。这是大概的思路,不过其中的问题很多,我们慢慢道来。
原理
mprotect函数
mprotect函数的原型如下:
int mprotect(const void *addr, size_t len, int prot);
其中addr是待保护的内存首地址,必须按页对齐;len是待保护内存的大小,必须是页的整数倍,prot代表模式,可能的取值有PROT_READ(表示可读)、PROT_WRITE(可写)等。
不同体系结构和操作系统,一页的大小不尽相同。如何获得页大小呢?通过PAGE_SIZE宏或者getpagesize()系统调用即可。
定制中断处理函数
当线程试图对我们已保护(成只读)的内存进行篡改时,默认情况下程序会收到SIGSEGV错误而退出。能不能不退出并且把相应的调用栈打印出来分析?当然可以。通过如下代码注册你定制的中断处理函数即可:
struct sigaction act;
act.sa_sigaction = your_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
if(sigaction(SIGSEGV, &act, NULL) == -1) {
perror("Register hanlder failed");
exit(EXIT_FAILURE);
}
这样,控制流就会到达你编写的your_handler函数上。而your_handler的函数原型是:
void your_handler(int sig, siginfo_t *si, void *unused);
编写your_handler函数即可?是的,不过这里面有两个注意事项:
1,中断处理函数里不应该调用内存分配函数,否则可能会引起double fault。因此,不适合调用backtrace_symbols(内部会动态分配内存),而是通过backtrace_symbols_fd直接将调用栈信息直接刷到文件中。
2,中断处理函数中应该恢复被保护内存为可写,否则会引起死循环。(再次中断并进入咱们编写的函数)
封装
为了方便使用,我封装了一个类,供参考:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class MemoryDetector
{
public:
typedef void (*segv_handler) (int sig, siginfo_t *si, void *unused);
static void init(const char *path)
{
register_handler(handler);
fd_ = open(path, O_RDWR|O_CREAT, 777);
}
static int protect(void *p, int len)
{
address_ = reinterpret_cast<uint64_t>(p);
len_ = len;
uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ);
}
static int umprotect(void *p, int len)
{
uint64_t tmp_address_ = reinterpret_cast<uint64_t>(p);
uint64_t start_address = (tmp_address_ >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE);
}
static int umprotect()
{
uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE);
}
static void finish()
{
close(fd_);
}
private:
static void register_handler(segv_handler sh)
{
struct sigaction act;
act.sa_sigaction = sh;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
if(sigaction(SIGSEGV, &act, NULL) == -1){
perror("Register hanlder failed");
exit(EXIT_FAILURE);
}
}
static void handler(int sig, siginfo_t *si, void *unused)
{
uint64_t address = reinterpret_cast<uint64_t>(si->si_addr);
if (address >= address_ && address < address_ + len_) {
umprotect(si->si_addr, PAGE_SIZE);
my_backtrace();
}
}
static void my_backtrace()
{
const int N = 100;
void* array[100];
size_t size = backtrace(array, N);
backtrace_symbols_fd(array, size, fd_);
}
static uint64_t address_;
static int len_;
static int fd_;
};
|
这个封装还存在一些问题,比如缺少错误处理,待保护内存必须在一页内等。读者诸君可以根据需要自行完善。
实战
来个例子,实战一下吧
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
27
28
29
30
|
#include "test.h" //就是上面封装的MemoryDetector类
#include
using namespace std;
uint64_t MemoryDetector::address_ = 0;
int MemoryDetector::len_ = 0;
int MemoryDetector::fd_ = 0;
///////////////////////////////////////
int *p = NULL;
void g()
{
usleep(2000000);
char *q = reinterpret_cast<char *>(p);
*(q+2) = 111;//非法篡改!!!
}
void f()
{
p = new int(1);
MemoryDetector::protect(p, 4);
}
int main()
{
const char *path = "result.tmp";//调用栈信息存放路径
MemoryDetector::init(path);
std::thread t1(f);
std::thread t2(g);
t1.join();
t2.join();
MemoryDetector::finish();
return 0;
}
|
用如下方式编译链接以上程序:
g++ -g -rdynamic -std=c++11 -pthread test.cpp -o test
程序运行结束后,打开result.tmp文件,看到如下内容:
./test(_ZN14MemoryDetector12my_backtraceEv+0x26)[0x405ce8]
./test(_ZN14MemoryDetector7handlerEiP7siginfoPv+0x60)[0x405cc0]
/lib64/libpthread.so.0[0x339a80f500]
./test(_Z1gv+0x25)[0x405909]
./test(_ZNSt6thread5_ImplIPFvvEE6_M_runEv+0x16)[0x406e2c]
/usr/lib64/libstdc++.so.6[0x3a6f6b6490]
/lib64/libpthread.so.0[0x339a807851]
/lib64/libc.so.6(clone+0x6d)[0x339a4e767d]
注意其中的第四行:./test(_Z1gv+0x25)[0x405909]。使用addr2line命令:
addr2line -e test 0x405909
获得非法篡改的代码位置:
/home/yebangyu/test.cpp:13
真相大白了。