glibc2.36
引入 (lazy binding)
下文的分析将以如下代码为例
# include<stdio.h>
int main(){
char c;
c = getchar();
printf("c: %c\n", c);
return 0;
}
//gcc -g dl.c -no-pie -o dl
在 gdb 中下断点 b 6
使函数停 printf 执行前
一步步执行可以看见跳转到了 printf@plt
所在的地方,这里是一段跳转的代码,将要跳转到 got.plt 中存放的地址。不过此时 got 还未初始化,存放的不是真实的地址
而是将 printf 的偏移(0)入栈,再跳转到 0x401020 (这是什么固定的地址吗?),将 link_map 入栈,最后跳转到 _dl_runtime_resolve_xsavec
通过以上的调试过程可以知道
-
plt
是过程链接表,存放跳转时候的代码
-
got
完成初始化之后存放真实地址,未初始化时是 plt 附近的地址
后续 _dl_runtime_resolve_xsavec
将会调用 _dl_fixup
查找真正的函数地址并写入 got 表,具体的过程结合源码分析。
结构
节表
可以用 readelf -d dl
命令查看所有节表
其中几个比较关键
-
STRTAB
字符串表
-
SYMTAB
符号表,可以用
readelf -s dl
查看 -
PLTGOT
就是常说的 got 表
-
JUMPREL
函数重定位表,可以用
readelf -r dl
查看Relocation section '.rela.plt' at offset 0x508 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000404020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0
结构体
link_map
这是链接器(linker)为每一共享库创建一个 link_map 结构体,所有 link_map 组织成链表。定义在 inlude/link.h
中,太长了所以只摘取了部分。
struct link_map
{
//libc 基址
ElfW(Addr) l_addr;
...
//dynamic section
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
...
//version
struct r_found_version *l_versions;
可以在 gdb 中查看这个链结构,具体做法是把 dl-runtime.c 文件放在同一个目录下后(如果编译时没有保留 -g
选项要导入一下调试信息)就可以在 gdb 看见源码
当执行到结构体附近的代码时可以打印结构体信息,这里有一个小技巧,set print pretty on
能打印详细的结构体信息。
也可以用 ->
访问结构体成员,比如下一个 link_map 的名字是
Elf64_Rel
每一个需要重定位的函数都有一个 Elf64_Rela 结构体,存储了重定位所需要的信息,这些 Elf64_Rela 组成了 JUMPREL 。
-
r_offset
它包含解析符号的地址将存储的位置(在GOT中)。
-
r_info
指示重新定位类型并充当符号表索引。它将用于在 DYNSYM 部分中查找相应的Elf64_Sym结构。
printf 对应的 reloc 为,我们可以在前文 JUMPREL 中看见同样的结构。
Elf64_Dyn
记录 dynamic table 信息
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
Elf64_Sym
描述每个符号的结构体
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
_dl_fixup
完整代码
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL])
+ reloc_offset (pltgot, reloc_arg));
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;
/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}
/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);
if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}
可以看到,这个函数接收两个参数,分别是我们在引入的小节中 push 的参数,link_map l 存放 libc 的信息,reloc_arg 是在 JUPREL 用于查找的序号。
symtab&strtab&pltgot&reloc
函数首先从 link_map 中找到以上节表的地址,我们先来看第一条解析 symtab 的语句。
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
这里赋值了一个指向常量 ElfW(Sym)
类型的常量指针 symtab。根据宏,类型ELFw(Sym)
可以解析成 Elf64_sym
#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t) // 64 位 w = 64
#define _ElfW_1(e,w,t) e##w##t // ## 用于连接成符号
至于 symtab 是如何得到的呢?在 elf/elf.h 中定义了DT_SYMTAB
为常量 6,即 l_info[6]
表示了第六个 ”Dynamic section entry“ 不会翻译(
#define DT_PLTGOT 3 /* Processor defined value */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_RELA 7 /* Address of Rela relocs */
而 D_PTR 的宏如下
# define D_PTR(map, i) (map)->i->d_un.d_ptr
因此 D_PTR (l, l_info[DT_SYMTAB])
表示的是
D_PTR (l, l_info[DT_SYMTAB])
= l-> l_info[5]->d_un.d_ptr
即
pwndbg> p/x *l->l_info[6]
$57 = {
d_tag = 0x6,
d_un = {
d_val = 0x4003d0,
d_ptr = 0x4003d0
}
}
pwndbg> p/x l->l_info[6]->d_un->d_ptr
$58 = 0x4003d0
其值正好是 SYMTAB。获得 strtab&pltgot&reloc 变量的值的过程于此相似,不再赘述。
随后,根据 reloc (Elf64_Rel 结构体)函数重定位的信息 r_offset,r_offset 得到 got 表真实地址和符号信息
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
根据宏 #define ELF64_R_SYM(i) ((i) >> 32)
可以得知,这是取 r_info 前四字节,因此 sym 指向 symtab 的序号 2 的结构体。
l->l_addr
表示程序的加载基址,这里关了 pie 所以是 0,得到了 got 表地址 rel_addr。
两个检查
第一个检查是检查 r_info 的后一字节为 0x7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF_MACHINE_JMP_SLOT R_X86_64_JUMP_SLOT
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
//等价于
assert ((reloc->r_info & 0xff) == 0x7);
第二个检查是符号的 visibility 如果不满足 check 的话就表示 normal lookup rules are not used
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o)
#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
其次计算 version
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
寻找地址、填入 got
在 libc 中定位函数地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
加上 libc 的基址
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
写入 got 表
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
ret2dl_resolve
再说吧
后话
clion 怎么跳转到定义
https://syst3mfailure.io/ret2dl_resolve/
clion不支持代码跳转 不同文件夹之间clion无法代码跳转? - 知乎用户的回答 - 知乎 https://www.zhihu.com/question/50223053/answer/752977021
标签:info,resolve,const,sym,reloc,DT,ret2dl,ElfW From: https://www.cnblogs.com/91ac0m0/p/17643991.html