借助/proc/pid/mem优化Android nativ

背景

在实现Android native crash捕获功能的时候,需要读取crash进程的内存数据,比如在计算elf的 load bias、Build-Id时,需要读取一块内存区域的数据,比如在获取以某个寄存器值为地址的附近内存数据时,也需要读取一块内存数据。之前我实现的时候是使用ptrace系统调用来读取crash进程数据的,当时查看了下,似乎ptrace不支持一次读取一块内存数据,所以当时我是多次PTRACE_PEEKDATA来实现的,也就是多次调用下面的方法来获取crash进程的一块内存数据的:

1
2
3
4
5
6
7
8
c++复制代码std::optional<long> readData(pid_t pid, void* addr) {
errno = 0;
long data = ptrace(PTRACE_PEEKDATA, pid, addr, nullptr);
if (errno != 0) {
return {};
}
return data;
}

上周在写相关文章:Android native crash sdk实现之crash捕获&tombstone信息的生成的时候,又想起来这个事情,感觉应该是有方法能够一次获取一个 memory block的。

/proc/pid/mem

通过搜索发现 Linux 上有个文件:/proc/pid/mem,我们可以借助openlseekread 来访问目标进程的内存数据:

  1. 通过 open 打开 /proc/pid/mem,便可借助打开的这个fd访问目标进程的虚拟内存
  2. 通过 lseek 可以定位到指定的虚拟地址处
  3. 通过 read 可以读取一个内存块

当然打开/proc/pid/mem肯定是需要权限的,否则就可以获取任意进程的内存数据了。不过我们的dumper进程是crash进程的子进程,可以PTRACE_ATTACH到crash进程,也有权限open/proc/pid/mem

一次读取一个memory block

可以按如下方式打开crash进程的 mem fd:

1
2
3
4
5
c++复制代码int openMemFd(pid_t targetPid) {
char path[32];
snprintf(path, sizeof(path), "/proc/%d/mem", targetPid);
return open(path, O_RDONLY);
}

可以按如下方式一次读取一个 memory block:

1
2
3
4
5
6
7
8
9
10
11
12
c++复制代码ssize_t readMemoryBlock(int memfd, uint64_t addr, void* buf, size_t len) {
if (!buf) {
return -1;
}

if (lseek(memfd, addr, SEEK_SET) == -1) {
LOGE("lseek failed: %s", strerror(errno));
return -1;
}

return read(memfd, buf, len);
}

以计算elf build-id 为例

上面提供了一个readMemoryBlock方法可以一次读取一个memory block,我们以计算elf build-id 为例看下使用:

  1. 读取 elf header:
1
2
3
4
5
c++复制代码ElfW(Ehdr) ehdr;
if (readMemoryBlock(memfd, mapAddr, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) {
LOGE("failed read elf header through memfd");
return;
}
  1. 读取 program header table:
1
2
3
4
5
6
7
8
9
10
11
c++复制代码auto phdrAddr = mapAddr + ehdr.e_phoff;
auto phdrSize = ehdr.e_phnum * ehdr.e_phentsize;
auto phdrData = malloc(phdrSize);
if (!phdrData) {
return;
}
if (readMemoryBlock(memfd, phdrAddr, phdrData, phdrSize) != phdrSize) {
LOGE("failed read phdr through memfd");
free(phdrData);
return;
}
  1. 读取 PT_NOTE 并计算 build-id:
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
c++复制代码for (int i = 0; i < ehdr.e_phnum; ++i) {
auto phdr = *(ElfW(Phdr)*)((uint64_t)phdrData + i * ehdr.e_phentsize);
switch (phdr.p_type) {
case PT_NOTE: {
auto notePtr = (const char*)(loadBias_ + phdr.p_vaddr);
auto noteData = malloc(phdr.p_memsz);
if (readMemoryBlock(memfd, (uint64_t)notePtr, noteData, phdr.p_memsz) != phdr.p_memsz) {
LOGE("failed read note through memfd");
} else {
auto noteStart = (const char*)noteData;
auto noteEnd = noteStart + phdr.p_memsz;
do {
auto note = *(ElfW(Nhdr)*) noteStart;
if (strncmp(noteStart + sizeof(ElfW(Nhdr)), "GNU", sizeof("GNU")) == 0) {
auto descStart = noteStart + sizeof(ElfW(Nhdr)) + note.n_namesz;
auto descEnd = descStart + note.n_descsz;

std::stringstream buf;
buf.fill('0');
buf.setf(std::ios_base::hex, std::ios_base::basefield);
for (; descStart < descEnd; ++descStart) {
buf.width(2);
buf << (uint32_t) *descStart;
}
buildId_ = buf.str();
break;
}

noteStart += sizeof(ElfW(Nhdr)) + note.n_namesz + note.n_descsz;
} while (noteStart < noteEnd);
}
free(noteData);
break;
}
}
}

优势

相比多次调用ptrace系统调用,性能上应该会有优化,另外多次的系统调用在错误处理方面也会比较麻烦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%