在用户空间中加载 C 中的 ELF 文件
Posted
技术标签:
【中文标题】在用户空间中加载 C 中的 ELF 文件【英文标题】:loading ELF file in C in user space 【发布时间】:2012-12-04 04:50:15 【问题描述】:我正在尝试在 64 位 x86 环境中的 Linux 上加载使用“gcc -m32 test.c -o test.exe”编译的 ELF 文件。我正在尝试在具有以下核心逻辑(32 位 ELF)的用户空间 ELF 加载器中加载该 32 位文件(test.exe)。
问题是调用返回的起始地址会导致 分段错误核心转储。代码如下:
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...
...
【问题讨论】:
printk
建议使用内核模块,而 mmap
和 mprotect
是用户空间。如果您正在编写用户空间应用程序,您是否考虑过使用 gcc -Wall -g
编译它并使用 gdb
调试它?并且 ELF 图像的起始地址不是main
例程(而是一些_start
在一些crt0.o
)
【参考方案1】:
请提供完整的可运行代码,包括您尝试加载的 ELF。 我已经花时间尽可能地修改你的代码,它似乎工作,至少对于这个简单的代码。
请注意,加载器也必须编译为 32 位代码,不能将 32 位文件加载到 64 位进程中。此外,由于您没有在原始位置加载代码,因此它必须是可重定位的。最后,它必须是静态二进制文件,因为您没有加载任何库。
更新:您的代码期望加载代码的入口点符合int (*main)(int, char **)
原型,但通常情况并非如此(旁注:main
实际上得到了第三个参数,环境也是如此)。阅读startup state of ELF。如果您手动创建那里描述的堆栈布局,您必须跳转到入口点,并且永远不会返回。如果是 C 程序,您可以挖掘出main
的地址,这将与原型匹配。但是,您随后会跳过 C 库的初始化(请记住,您的代码不会加载库,因此加载的程序必须是静态链接的),这可能是个问题。
通过解析 libc 引用并调用 main
,我已使用处理简单 C 程序所需的位扩展了代码。
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
【讨论】:
感谢您的帮助。问题似乎出在您尝试加载 C 程序(甚至是打印 hello world 的简单程序)时。我的猜测是直接调用 C _start 例程有问题,或者加载程序代码有一个错误重定位多个部分(在调用 memmove 的位置周围)。 对此很抱歉,但我只是想知道你们从哪里获得 libelf 库。谢谢 这是一个非常古老的线程,但是,您不需要担心代码中的页面对齐问题吗? @user1018513 在这里,我们正在加载由普通工具链创建的正确 ELF 二进制文件,因此所有对齐都已处理完毕。 这不是一种非常有效的做事方式。该缓冲区仅用于读取整个文件。你可以简单地stat它来获取大小。【参考方案2】:exec = (unsigned char *)mmap(NULL, size, ...
这会尝试在任意地址加载可执行文件。非 PIE 可执行文件只能在其链接的地址处加载(在 Linux/ix86 上通常为 0x08048000
)。
问题出现在您尝试加载 C 程序(甚至是打印 hello world 的简单程序)时。
如果该程序是动态链接的,那么它任何事情都很简单,而且你的加载器还有很多事情要做:加载和重定位相关的共享库,修复GOT
和TLS
,等等等等。
【讨论】:
【参考方案3】:使用
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);
【讨论】:
以上是关于在用户空间中加载 C 中的 ELF 文件的主要内容,如果未能解决你的问题,请参考以下文章
在无限滚动页面中加载不同维度的表格,同时最小化表格之间的空白空间
.bss 部分零初始化变量是不是占用 elf 文件中的空间?
mvtnorm无法在MS r-client版本3.4.3中加载