在用户空间中用C语言加载ELF文件

22

我正在尝试在64位x86环境下的Linux中加载使用 "gcc -m32 test.c -o test.exe"编译的ELF文件。我正在尝试将这个32位文件(test.exe)加载到具有以下核心逻辑(32位ELF)的用户空间ELF加载器中。

问题在于调用返回的起始地址会导致分段错误核心转储。以下是代码:

void *image_load (char *elf_start, unsigned int size)
{
    Elf32_Ehdr      *hdr    = NULL;
    Elf32_Phdr      *phdr   = NULL;
    unsigned char   *start  = NULL;
    Elf32_Addr      taddr   = 0;
    Elf32_Addr      offset  = 0;
    int i = 0;
    unsigned char *exec = NULL;
    Elf32_Addr      estart = 0;

    hdr = (Elf32_Ehdr *) elf_start;

    if(!is_image_valid(hdr)) {
        printk("image_load:: invalid ELF image\n");
        return 0;
    }

    exec = (unsigned char *)mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                      MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

    if(!exec) {
        printk("image_load:: error allocating memory\n");
        return 0;
    }

    // Start with clean memory.
    memset(exec,0x0,size);

    phdr = (Elf32_Phdr *)(elf_start + hdr->e_phoff);

    for(i=0; i < hdr->e_phnum; ++i) {
            if(phdr[i].p_type != PT_LOAD) {
                    continue;
            }
            if(phdr[i].p_filesz > phdr[i].p_memsz) {
                    printk("image_load:: p_filesz > p_memsz\n");
                    munmap(exec, size);
                    return 0;
            }
            if(!phdr[i].p_filesz) {
                    continue;
            }

            // p_filesz can be smaller than p_memsz,
            // the difference is zeroe'd out.
            start = (unsigned char *) (elf_start + phdr[i].p_offset);
            // taddr = phdr[i].p_vaddr + (Elf32_Addr)exec;
            if(!estart) {
                estart = phdr[i].p_paddr;
            }
            taddr = (Elf32_Addr)exec + offset + (phdr[i].p_paddr - estart);
            memmove((unsigned char *)taddr,
                    (unsigned char *)start,phdr[i].p_filesz);
            offset += (phdr[i].p_memsz + (phdr[i].p_paddr - estart));

            if(!(phdr[i].p_flags & PF_W)) {
                    // Read-only.
                    mprotect((unsigned char *) taddr, 
                              phdr[i].p_memsz,
                              PROT_READ);
            }

            if(phdr[i].p_flags & PF_X) {
                    // Executable.
                    mprotect((unsigned char *) taddr, 
                            phdr[i].p_memsz,
                            PROT_EXEC);
            }
    }

    return (void *)((hdr->e_entry - estart) + (Elf32_Addr)exec);

}/* image_load */

...
    int (*main)(int, char **)=image_load(...);
    main(argc,argv); // Crash...
...

1
printk 建议用于内核模块,而 mmapmprotect 适用于用户空间。如果您正在编写用户空间应用程序,请考虑使用 gcc -Wall -g 进行编译,并使用 gdb 进行调试。ELF 图像的起始地址 不是 main 程序(而是一些 crt0.o 中的某个 _start - Basile Starynkevitch
3个回答

31

请提供完整可运行的代码,包括您想要加载的ELF文件。

我已经尽力修改了您的代码,并且它似乎至少对于这个简单的代码是有效的。请注意,加载器必须编译为32位代码,因为您不能将32位文件加载到64位进程中。此外,由于您没有在原始位置加载代码,因此它必须是可重定位的。最后,它必须是一个静态二进制文件,因为您没有加载任何库。

更新:您的代码期望加载的代码入口符合int (* main) (int, char **)原型,但一般情况下并非如此(旁注:main实际上有第三个参数,环境变量)。请阅读有关ELF启动状态的文章。如果您手动创建描述那里的堆栈布局,则必须跳转到入口点,并且永远不会返回。对于C程序,您可以挖出main的地址,这将匹配原型。但是,您正在跳过C库的初始化(请记住,您的代码不进行库加载,因此加载的程序必须静态链接),这可能是个问题。

我已经添加了必要的部分来处理一个简单的C程序,通过解决libc引用并调用main

loader.c:

#include <stdio.h>
#include <string.h>
#include <libelf.h>
#include <sys/mman.h>
#include <dlfcn.h>

void printk(const char* msg)
{
    fputs(msg, stderr);
}

int is_image_valid(Elf32_Ehdr *hdr)
{
    return 1;
}

void *resolve(const char* sym)
{
    static void *handle = NULL;
    if (handle == NULL) {
        handle = dlopen("libc.so", RTLD_NOW);
    }
    return dlsym(handle, sym);
}

void relocate(Elf32_Shdr* shdr, const Elf32_Sym* syms, const char* strings, const char* src, char* dst)
{
    Elf32_Rel* rel = (Elf32_Rel*)(src + shdr->sh_offset);
    int j;
    for(j = 0; j < shdr->sh_size / sizeof(Elf32_Rel); j += 1) {
        const char* sym = strings + syms[ELF32_R_SYM(rel[j].r_info)].st_name;
        switch(ELF32_R_TYPE(rel[j].r_info)) {
            case R_386_JMP_SLOT:
            case R_386_GLOB_DAT:
                *(Elf32_Word*)(dst + rel[j].r_offset) = (Elf32_Word)resolve(sym);
                break;
        }
    }
}

void* find_sym(const char* name, Elf32_Shdr* shdr, const char* strings, const char* src, char* dst)
{
    Elf32_Sym* syms = (Elf32_Sym*)(src + shdr->sh_offset);
    int i;
    for(i = 0; i < shdr->sh_size / sizeof(Elf32_Sym); i += 1) {
        if (strcmp(name, strings + syms[i].st_name) == 0) {
            return dst + syms[i].st_value;
        }
    }
    return NULL;
}

void *image_load (char *elf_start, unsigned int size)
{
    Elf32_Ehdr      *hdr     = NULL;
    Elf32_Phdr      *phdr    = NULL;
    Elf32_Shdr      *shdr    = NULL;
    Elf32_Sym       *syms    = NULL;
    char            *strings = NULL;
    char            *start   = NULL;
    char            *taddr   = NULL;
    void            *entry   = NULL;
    int i = 0;
    char *exec = NULL;

    hdr = (Elf32_Ehdr *) elf_start;

    if(!is_image_valid(hdr)) {
        printk("image_load:: invalid ELF image\n");
        return 0;
    }

    exec = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                      MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

    if(!exec) {
        printk("image_load:: error allocating memory\n");
        return 0;
    }

    // Start with clean memory.
    memset(exec,0x0,size);

    phdr = (Elf32_Phdr *)(elf_start + hdr->e_phoff);

    for(i=0; i < hdr->e_phnum; ++i) {
            if(phdr[i].p_type != PT_LOAD) {
                    continue;
            }
            if(phdr[i].p_filesz > phdr[i].p_memsz) {
                    printk("image_load:: p_filesz > p_memsz\n");
                    munmap(exec, size);
                    return 0;
            }
            if(!phdr[i].p_filesz) {
                    continue;
            }

            // p_filesz can be smaller than p_memsz,
            // the difference is zeroe'd out.
            start = elf_start + phdr[i].p_offset;
            taddr = phdr[i].p_vaddr + exec;
            memmove(taddr,start,phdr[i].p_filesz);

            if(!(phdr[i].p_flags & PF_W)) {
                    // Read-only.
                    mprotect((unsigned char *) taddr,
                              phdr[i].p_memsz,
                              PROT_READ);
            }

            if(phdr[i].p_flags & PF_X) {
                    // Executable.
                    mprotect((unsigned char *) taddr,
                            phdr[i].p_memsz,
                            PROT_EXEC);
            }
    }

    shdr = (Elf32_Shdr *)(elf_start + hdr->e_shoff);

    for(i=0; i < hdr->e_shnum; ++i) {
        if (shdr[i].sh_type == SHT_DYNSYM) {
            syms = (Elf32_Sym*)(elf_start + shdr[i].sh_offset);
            strings = elf_start + shdr[shdr[i].sh_link].sh_offset;
            entry = find_sym("main", shdr + i, strings, elf_start, exec);
            break;
        }
    }

    for(i=0; i < hdr->e_shnum; ++i) {
        if (shdr[i].sh_type == SHT_REL) {
            relocate(shdr + i, syms, strings, elf_start, exec);
        }
    }

   return entry;

}/* image_load */

int main(int argc, char** argv, char** envp)
{
    int (*ptr)(int, char **, char**);
    static char buf[1048576];
    FILE* elf = fopen(argv[1], "rb");
    fread(buf, sizeof buf, 1, elf);
    ptr=image_load(buf, sizeof buf);
    return ptr(argc,argv,envp);
}

elf.c:

#include <stdio.h>

int main()
{
    fprintf(stdout, "Hello world! fprintf=%p, stdout=%p\n", fprintf, stdout);
    return 0;
}
测试运行:
$ gcc -m32 -g -Wall -ldl -o loader loader.c
$ gcc -m32 -pie -fPIE -o elf elf.c
$ ./loader elf
Hello world! fprintf=0xf7612420, stdout=0xf770e4c0

1
感谢您的帮助。问题似乎出现在尝试加载C程序时(甚至是打印“hello world”这样简单的程序)。我猜测可能是直接调用C语言的_start例程存在问题,或者加载器代码在重定位超过一个段(在调用memmove的位置附近)时存在错误。 - Smokey
抱歉,我只是想知道你们从哪里获取libelf库。谢谢。 - Kimutai
1
这是一个非常古老的帖子,但是,在你的代码中不需要担心页面对齐的问题吗? - user1018513
@user1018513,这里我们正在加载由正常工具链创建的适当ELF二进制文件,因此所有对齐都已经处理好了。 - Jester
这不是一种非常高效的做事方式。那个缓冲区仅用于读取整个文件。你可以简单地使用 stat 获取其大小。 - Jester
抱歉,我指的是mmap的大小。它是否为所有p_memsz值的总和?另外,如果我只是尝试加载自己的共享库,不链接任何其他内容并且没有libc,我需要进行重定位吗? - Edward Chamberlain

6

exec = (unsigned char *)mmap(NULL, size, ...

这段代码尝试在任意地址加载可执行文件。非PIE(位置独立可执行)的可执行文件只能以链接时的地址(在Linux/ix86上通常是0x08048000)加载。

问题出现在你尝试加载一个C程序时(即使是打印Hello World的简单程序)。

如果那个程序是动态链接的,它绝非简单,而且你的加载器有很多工作要做:加载和重定位依赖的共享库,修复GOTTLS等等。


2

使用

exec = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                  MAP_PRIVATE | MAP_ANONYMOUS, hdr, 0);

替代,而不是
exec = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                  MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接