You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自制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原生加载器之所以能成功,核心原因是:

  1. 它不会用MAP_FIXED强制固定地址,允许内核自动处理相邻映射的合并或地址调整
  2. 内核对于地址重叠的情况会智能处理,要么扩展已有映射,要么在指定地址不可用时自动选择合适的替代地址

修复方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:50:12