自制ELF64加载器mmap映射0x4a5000失败,Linux加载器却成功
自制ELF64加载器映射地址失败问题分析
我用GCC编译ELF64程序后,自制加载器在尝试映射地址0x4a5000时失败,失败时的映射信息如下:Mapping attempt: vaddr=0x4a5000, filesz=23416, prot=3, fd=3, offset=675664
映射记录对比
自制加载器失败前的映射记录
Start Addr End Addr Size Offset Perms objfile 0x400000 0x401000 0x1000 0x0 r--p /home/quentin/elf_loader/bin/payload_c 0x401000 0x47f000 0x7e000 0x1000 r-xp /home/quentin/elf_loader/bin/payload_c 0x47f000 0x4a5000 0x26000 0x7f000 r--p /home/quentin/elf_loader/bin/payload_c
Linux原生ELF加载器的映射记录
Start Addr End Addr Size Offset Perms objfile 0x400000 0x401000 0x1000 0x0 r--p /home/quentin/elf_loader/bin/payload_c 0x401000 0x47f000 0x7e000 0x1000 r-xp /home/quentin/elf_loader/bin/payload_c 0x47f000 0x4a5000 0x26000 0x7f000 r--p /home/quentin/elf_loader/bin/payload_c 0x4a5000 0x4aa000 0x5000 0xa4000 r--p /home/quentin/elf_loader/bin/payload_c 0x4aa000 0x4ac000 0x2000 0xa9000 rw-p /home/quentin/elf_loader/bin/payload_c
自制加载器实现代码
int phdr_map(Elf64_Phdr phdr, FILE *fd) { int raw_fd = fileno(fd); // This segment mustn't be mapped in memory if (!(phdr.p_type & PT_LOAD) || phdr.p_vaddr == 0) return 0; int mmap_prot = phdr_map_prot(phdr); void *addr = (void *)align(phdr.p_vaddr, phdr.p_align); off_t offset = phdr.p_offset; void *mapped = mmap(addr, phdr.p_filesz, mmap_prot, MAP_PRIVATE | MAP_FIXED, raw_fd, offset); if (mapped == MAP_FAILED) { fprintf(stderr, "Mapping attempt: vaddr=%p, filesz=%lu, prot=%d, fd=%d, offset=%lu\n", addr, phdr.p_filesz, mmap_prot, raw_fd, offset); exit(1); } return 1; }
问题原因
从映射记录能明显看到:自制加载器最后一个映射的结束地址正好是0x4a5000,也就是下一个要映射的目标地址。而代码中使用了MAP_FIXED标志,该标志会强制要求mmap必须将内存映射到指定地址,一旦该地址已经属于已映射的内存区域,就会直接失败。
Linux原生加载器之所以能成功,核心原因是:
- 它不会用
MAP_FIXED强制固定地址,允许内核自动处理相邻映射的合并或地址调整 - 内核对于地址重叠的情况会智能处理,要么扩展已有映射,要么在指定地址不可用时自动选择合适的替代地址
修复方案
1. 移除MAP_FIXED标志
这是解决映射冲突的关键。去掉该标志后,内核会优先尝试使用phdr.p_vaddr指定的地址,只有当该地址被占用时才会自动分配其他可用地址(对于正常的ELF程序,指定地址通常不会被占用)。
2. 处理p_memsz与p_filesz的差异
ELF段的p_memsz可能大于p_filesz(比如BSS段,文件中无内容但需要在内存中分配零填充区域),原有代码只映射了文件中的内容,需要补充处理内存中额外的区域。
修改后的代码示例
int phdr_map(Elf64_Phdr phdr, FILE *fd) { int raw_fd = fileno(fd); if (!(phdr.p_type & PT_LOAD) || phdr.p_vaddr == 0) return 0; int mmap_prot = phdr_map_prot(phdr); void *addr = (void *)align(phdr.p_vaddr, phdr.p_align); off_t offset = phdr.p_offset; // 移除MAP_FIXED,让内核自动处理地址分配 void *mapped = mmap(addr, phdr.p_filesz, mmap_prot, MAP_PRIVATE, raw_fd, offset); if (mapped == MAP_FAILED) { fprintf(stderr, "Mapping attempt: vaddr=%p, filesz=%lu, prot=%d, fd=%d, offset=%lu\n", addr, phdr.p_filesz, mmap_prot, raw_fd, offset); exit(1); } // 处理内存大小大于文件大小的情况(如BSS段) if (phdr.p_memsz > phdr.p_filesz) { size_t extra_size = phdr.p_memsz - phdr.p_filesz; void *extra_addr = (void *)((uintptr_t)mapped + phdr.p_filesz); // 映射匿名页并填充零 void *extra_mapped = mmap(extra_addr, extra_size, mmap_prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); if (extra_mapped == MAP_FAILED) { fprintf(stderr, "Failed to map extra memory for segment: vaddr=%p, size=%zu\n", extra_addr, extra_size); exit(1); } } return 1; }
内容的提问来源于stack exchange,提问作者Quentin
相关产品推荐
相关产品推荐

