[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 ;
    }
  }
}
posted @ 2022-04-19 00:37  新新人類  阅读(171)  评论(0编辑  收藏  举报