Linux用户态程序[vvar]段转储:解决SIGBUS与映射页识别难题
问题
需要从x86、ARM或MIPS架构的Linux用户态程序中转储[vvar]段,但遇到以下问题:
[vvar]段仅前几页被映射,访问未填充页面会触发SIGBUS信号。- 尝试安装
SIGBUS信号处理程序:返回后未映射页面仍未填充,会递归触发SIGBUS,无法恢复。 - 尝试用
/proc/self/pagemap识别已映射页面:该文件对[vvar]页无效,返回全0,无法判断页面是否填充。
简化代码如下:
// 从/proc/self/maps读取段信息: get_local_segment_info("[vvar]", &e); // 返回 e.start_addr=0x7fffb4b6e000, e.size=16384 ASSERT(e.start_addr != NULL); #if 1 gzwrite(gz_fd, e.start_addr, e.size); // 触发SIGBUS失败 #else self_pagemap_fd = open("/proc/self/pagemap",O_RDONLY); ASSERT(self_pagemap_fd >= 0); off64_t off = lseek64(self_pagemap_fd , (((uintptr_t)e.start_addr) / page_size) * sizeof(uint64_t), SEEK_SET); ASSERTf(off >= 0, "ERROR: could not lseek64 (%s)",strerror(errno)); size_t nread = read( self_pagemap_fd , (uint8_t *)pagemaps, sizeof(uint64_t) * num_pages); ASSERT(nread > 0); printf("pagemap[0]=0x%" PRIx64 "\n", pagemaps[0]); // 输出 pagemap[0]=0x0 ... // 本应遍历页面仅复制已填充的部分,但pagemaps全为0,无法判断 #endif
可行解决方案
1. 用mincore检测已驻留页面
mincore系统调用可查询指定内存区域中哪些页面已加载到物理内存(即已映射),不受[vvar]特殊属性限制。
示例代码片段:
size_t page_size = sysconf(_SC_PAGESIZE); size_t num_pages = (e.size + page_size - 1) / page_size; unsigned char *vec = malloc(num_pages); if (!vec) { /* 错误处理 */ } // 调用mincore获取驻留页面信息 if (mincore(e.start_addr, e.size, vec) == -1) { perror("mincore failed"); free(vec); return; } // 遍历每个页面,仅复制已驻留的 for (size_t i = 0; i < num_pages; i++) { if (vec[i] & 1) { // 位0表示页面已驻留 uintptr_t page_addr = e.start_addr + i * page_size; size_t copy_size = (i == num_pages - 1) ? (e.size % page_size) : page_size; gzwrite(gz_fd, (void*)page_addr, copy_size); } else { // 未驻留页面,可写入0或跳过,根据需求处理 unsigned char zero_buf[page_size] = {0}; size_t copy_size = (i == num_pages - 1) ? (e.size % page_size) : page_size; gzwrite(gz_fd, zero_buf, copy_size); } } free(vec);
2. 改进SIGBUS信号处理程序,用siglongjmp跳过故障页面
直接返回会重复触发SIGBUS,可结合setjmp和siglongjmp在触发SIGBUS时跳转到安全点,跳过当前故障页面。注意信号处理程序中需使用异步信号安全的函数。
示例代码片段:
#include <setjmp.h> static sigjmp_buf jmp_buf; static volatile sig_atomic_t fault_occurred = 0; static uintptr_t current_fault_addr = 0; static void sigbus_handler(int sig, siginfo_t *info, void *ctx) { fault_occurred = 1; current_fault_addr = (uintptr_t)info->si_addr; siglongjmp(jmp_buf, 1); } // 注册信号处理程序 struct sigaction sa = {0}; sa.sa_sigaction = sigbus_handler; sa.sa_flags = SA_SIGINFO; if (sigaction(SIGBUS, &sa, NULL) == -1) { perror("sigaction failed"); return; } size_t page_size = sysconf(_SC_PAGESIZE); uintptr_t addr = e.start_addr; size_t remaining = e.size; while (remaining > 0) { fault_occurred = 0; size_t copy_size = (remaining > page_size) ? page_size : remaining; if (sigsetjmp(jmp_buf, 1) == 0) { gzwrite(gz_fd, (void*)addr, copy_size); addr += copy_size; remaining -= copy_size; } else { // 计算故障页面的起始地址,跳过该页 uintptr_t page_start = current_fault_addr & ~(page_size - 1); size_t skip_size = page_start + page_size - addr; // 写入对应长度的0或跳过 unsigned char zero_buf[page_size] = {0}; gzwrite(gz_fd, zero_buf, skip_size); addr += skip_size; remaining -= skip_size; } }
3. 读取/proc/self/mem指定范围
/proc/self/mem可直接访问进程内存空间,读取未映射页面时会返回EIO错误而非触发SIGBUS。可逐页读取,根据返回值判断页面是否存在。
示例代码片段:
int mem_fd = open("/proc/self/mem", O_RDONLY); if (mem_fd == -1) { perror("open /proc/self/mem failed"); return; } size_t page_size = sysconf(_SC_PAGESIZE); uintptr_t addr = e.start_addr; size_t remaining = e.size; while (remaining > 0) { size_t read_size = (remaining > page_size) ? page_size : remaining; unsigned char buf[page_size]; // 定位到目标地址 if (lseek64(mem_fd, addr, SEEK_SET) == -1) { perror("lseek64 failed"); close(mem_fd); return; } ssize_t n = read(mem_fd, buf, read_size); if (n == -1) { if (errno == EIO) { // 页面未映射,写入0 memset(buf, 0, read_size); gzwrite(gz_fd, buf, read_size); } else { perror("read failed"); close(mem_fd); return; } } else { gzwrite(gz_fd, buf, n); } addr += read_size; remaining -= read_size; } close(mem_fd);
内容的提问来源于stack exchange,提问作者HardcoreHenry
相关产品推荐
相关产品推荐

