[lab]csapp-link
linklab
utah 大学的lab, 对应的是 csapp chapter 7, 链接的内容, 此 lab 可以帮助我们理解 elf 文件的内容和动态连接库的原理.
要求我们解析elf文件, 对于一个动态连接库, 解析出其中的函数符号和它依赖的函数和变量符号.
比如对于有如下内容的 .c 文件编译出的so
int a;
extern int b;
int e(int i);
int do_something(int v) {
b = a;
a = v;
return e(b);
}
应该输出
do_something
a
b
(e)
分为3阶段, 1. 只输出函数符号 2. 函数符号+引用的变量, 3. 函数符号+引用的变量和函数.
基本原理:
elf 是一个代码和数据等信息的二进制文件, 头部是一个结构体(Elf64_Ehdr), 描述了 elf 中的内容, 后续不同类型的信息是以分段的方式存储的, 每段也有头部描述(Elf64_Shdr), 因此我们可以直接 mmap 将文件加载入内存, 然后根据 offset, 引用到不同的段.
为了更好的理解 elf 内容, 使用 readelf -a
打印段内容, objudump -d
解码汇编指令供2, 3阶段使用.
结构体相关内容在 elf.h 中定义.
阶段1:
.dynsym 段声明了动态连接库需要导出的符号, 每个符号由Elf64_Sym描述.
首先使用 ehdr 遍历 shdr, 找到 .dynsym (type 为 SHT_DYNSYM), 然后在段中遍历 sym, 找到 type 为 FUNC 且 Bind 为 GLOBAL 的符号, 将其输出.
/* Open the shared-library file */
fd = open(argv[1], O_RDONLY);
if (fd == -1)
fail("could not open file", errno);
/* Find out how big the file is: */
len = lseek(fd, 0, SEEK_END);
/* Map the whole file into memory: */
p = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
if (p == (void*)-1)
fail("mmap failed", errno);
/* Since the ELF file starts with an ELF header, the in-memory image
can be cast to a `Elf64_Ehdr *` to inspect it: */
ehdr = (Elf64_Ehdr *)p;
/* Check that we have the right kind of file: */
check_for_shared_object(ehdr);
/* Add a call to your work here */
shstrtab_shdr = ehdr->e_shoff + p + (ehdr->e_shnum - 1) * sizeof (Elf64_Shdr);
for (i=0;i<ehdr->e_shnum;++i) {
shdr = ehdr->e_shoff + p + i * sizeof (Elf64_Shdr);
if (shdr->sh_type == SHT_DYNSYM) {
// so dynamic symbol table header
dynsym_shdr = shdr;
} else if (shdr->sh_type == SHT_SYMTAB) {
// all symbol table header
symtab_shdr = shdr;
} else if (shdr->sh_type == SHT_RELA) {
if (rela_shdr == NULL) {
// rela section header
rela_shdr = shdr;
} else {
// rela plt section header
rela_plt_shdr = shdr;
}
}
}
// 这里由于符号输出顺序的问题, 使用 symtab , 里面包含导出符号在内的所有符号.
print_sym(symtab_shdr);
print_sym 如下
static void print_sym(Elf64_Shdr* sym_shdr) {
int sym_len, i;
Elf64_Sym* sym;
// sym_tab 后面一个段就是它对应的 str_tab, 它存放了符号的名称, 通过 st_name 作为下标访问.
char* str_base = AT_SEC(ehdr, (sym_shdr+1));
sym_len = sym_shdr->sh_size / sizeof(Elf64_Sym);
for (i=0;i<sym_len;++i) {
sym = AT_SEC(ehdr, sym_shdr) + i * sizeof (Elf64_Sym);
if (ELF64_ST_TYPE(sym->st_info) == STT_FUNC &&
ELF64_ST_BIND(sym->st_info) == STB_GLOBAL &&
sym->st_size > 0) {
// 一个全局的函数, 就是导出的函数符号
printf("%s\n", (char*)(str_base + sym->st_name));
// 第二, 三阶段
read_asm(sym->st_value);
}
}
}
阶段2, 3
函数所引用的符号并没有存在某个段中, 需要我们解析汇编指令, 根据相对寻址, 来判断是否引用到了别的变量和函数.
这时我们需要重定向段 .rela.dyn (相对寻址能在so加载入内存中, 动态确定地址), Elf64_Rela 来表示一个重定向项
lab为我们预先设定了好要解析指令的范围, ret(0xc3), jmpq(0xe9), jmp(0xeb), mov, movq, ret 表示函数返回, 我们解析也可以终止, jmp 作为函数调用, 两个指令的区别在于相对寻址的字大小.
对于阶段2, 我们要解析出 mov/movq 中相对寻址的情况, 然后在 .rela.dyn 段中找到它的名称在 .dynsym 中的下标, 最后在 .dynsym 中打印即可.
对于阶段3, 再加入代码重定向段 .rela.plt, (我的理解: 如果要调用一个导出的函数符号g
, 也需要一层重定向, 但函数定义g
已经给出, 只能额外加一层重定向段, 包裹一层代码g@plt
, 通过 bnd jmp
来转发调用). 我们在遇到 jmp 时, 就直接跳转到对于指令位置, 继续解析, 假如遇到了bnd jmp
, 就说明它是一个重定向的函数调用, 与数据符号相同, 在 .rela.plt 找到它就可以了.
代码的实现就是读取字节, 然后解析成指令,
static void read_asm(Elf64_Addr offset) {
unsigned char code_byte, *ptr, bytes[4];
unsigned int val;
int i, iter;
ptr = (void*)ehdr + offset - 1;
/*
// load 4 byte to val
for (i=3;i>=0;--i) {
bytes[i] = *(++ptr);
}
val = 0;
for (i=0;i<4;++i) {
val = bytes[i] + (val << 8);
}
*/
// 放置读取指令越界.
while (iter++ < 200) {
code_byte = *(++ptr);
switch (code_byte) {
case RET_CODE:
return;
case JMP_CODE:
// load 4 byte to val
ptr += (int)val;
break;
case JMPR_CODE:
bytes[0] = *(++ptr);
val = bytes[0];
ptr += (char)val;
break;
case MOVQ_PREFIX_CODE:
break;
case MOV1_CODE:
case MOV2_CODE:
case MOV3_CODE:
bytes[0] = *(++ptr);
if ((bytes[0] & 0xC0) == 0xC0) {
// pass
} else if ((bytes[0] & 0x7) == 0x5 && (bytes[0] & 0xC0) != 0xC0) {
// load 4 byte to val
if (code_byte == MOV3_CODE) {
// 相对寻址的数据
find_item_in_rela(rela_shdr, ((void*)ptr - (void*)ehdr) + val + 1);
}
} else if ((bytes[0] & 0x7) == 0x4) {
++ptr;
// pass
} else {
// pass
}
break;
case 0xF3:
// endbr64
ptr += 3;
break;
case 0xF2:
// bnd jmpq addr
// 相对寻址的函数
ptr += 2;
// load 4 byte to val
find_item_in_rela(rela_plt_shdr, ((void*)ptr - (void*)ehdr) + (int)val + 1);
return;
default:
fprintf(stderr, "unsupported asm instruction %x at %lx\n", code_byte, ((void*)ptr - (void*)ehdr) - offset);
break;
}
}
}
static void find_sym(Elf64_Shdr* sym_shdr, int i, int data) {
Elf64_Sym* sym;
char* str_base = AT_SEC(ehdr, (sym_shdr+1));
sym = AT_SEC(ehdr, sym_shdr) + i * sizeof (Elf64_Sym);
if (data) {
printf(" %s\n", str_base+sym->st_name);
} else {
printf(" (%s)\n", str_base+sym->st_name);
}
}
static void find_item_in_rela(Elf64_Shdr *p_rela_shdr, Elf64_Off off) {
int rela_len, i;
Elf64_Rela* rela;
rela_len = p_rela_shdr->sh_size / sizeof(Elf64_Rela);
for (i=0;i<rela_len;++i) {
rela = AT_SEC(ehdr, p_rela_shdr) + i * sizeof(Elf64_Rela);
if (rela->r_offset == off) {
// 遍历所有的 rela item, 如果 offset相等就打印.
find_sym(dynsym_shdr, ELF64_R_SYM(rela->r_info), p_rela_shdr == rela_shdr);
return ;
}
}
}