1. 程式人生 > >多執行緒記憶體問題分析之mprotect方法

多執行緒記憶體問題分析之mprotect方法

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 <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/user.h>
#include <execinfo.h>
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 <thread>
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

真相大白了。