背景
在实现Android native crash捕获功能的时候,需要读取crash进程的内存数据,比如在计算elf的 load bias、Build-Id时,需要读取一块内存区域的数据,比如在获取以某个寄存器值为地址的附近内存数据时,也需要读取一块内存数据。之前我实现的时候是使用ptrace
系统调用来读取crash进程数据的,当时查看了下,似乎ptrace不支持一次读取一块内存数据,所以当时我是多次PTRACE_PEEKDATA
来实现的,也就是多次调用下面的方法来获取crash进程的一块内存数据的:
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
,我们可以借助open
,lseek
,read
来访问目标进程的内存数据:
- 通过
open
打开/proc/pid/mem
,便可借助打开的这个fd访问目标进程的虚拟内存 - 通过
lseek
可以定位到指定的虚拟地址处 - 通过
read
可以读取一个内存块
当然打开/proc/pid/mem
肯定是需要权限的,否则就可以获取任意进程的内存数据了。不过我们的dumper进程是crash进程的子进程,可以PTRACE_ATTACH
到crash进程,也有权限open/proc/pid/mem
。
一次读取一个memory block
可以按如下方式打开crash进程的 mem fd:
c++
int openMemFd(pid_t targetPid) {
char path[32];
snprintf(path, sizeof(path), "/proc/%d/mem", targetPid);
return open(path, O_RDONLY);
}
可以按如下方式一次读取一个 memory block:
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 为例看下使用:
- 读取 elf header:
c++
ElfW(Ehdr) ehdr;
if (readMemoryBlock(memfd, mapAddr, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) {
LOGE("failed read elf header through memfd");
return;
}
- 读取 program header table:
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;
}
- 读取 PT_NOTE 并计算 build-id:
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
系统调用,性能上应该会有优化,另外多次的系统调用在错误处理方面也会比较麻烦