首页 > 其他分享 >ELF 文件与链接

ELF 文件与链接

时间:2024-07-02 16:56:46浏览次数:1  
标签:文件 00 int objdump ELF 64 print so 链接

ELF 文件与链接

Created: 2024-07-02T11:03+08:00
Published: 2024-07-02T16:44+08:00
Categories: OperatingSystem

目录

链接就是为了解决符号的问题,符号分为变量和函数。
关于什么是 section 什么是 segment 就不赘述了,可以参考文章[1],里面的细节太繁琐,
我觉得更重要的是知道每个 section 中需要记录的信息,编码的格式是次要的。

工具:readelfobjdump

objdump -h <file> 看 sections
readelf -S <file> 看 sections
readelf -s <file> 看符号表
objdump -t <file> 看符号表
objdump -d <file> 看汇编代码
objdump -r <file> 看重定位符号

readelfobjdump 是两个不同的工具,它们在分析和调试 ELF 文件时提供了不同的功能和信息。

readelf 是一个用于查看 ELF 文件结构和内容的工具。它提供了对 ELF 文件头、节头表、符号表、重定位表等结构的解析和显示。readelf 可以用于查看 ELF 文件的基本信息、节的属性、符号的绑定和类型、重定位信息等。它对于了解 ELF 文件的结构和元数据非常有用。

objdump 则是一个更强大的工具,它提供了比 readelf 更多的功能。除了显示 ELF 文件的结构和内容外,objdump 还可以反汇编可执行文件或目标文件的机器代码,并提供源代码与汇编代码之间的对应关系。它还可以显示符号表、调试信息、堆栈跟踪等。objdump 还支持多种输出格式,如十六进制、反汇编、源代码等。

因此,尽管 readelf 可以提供对 ELF 文件的基本解析和显示,但 objdump 提供了更多的功能,包括反汇编、源代码对应、调试信息等。根据具体的需求,选择适合的工具可以更好地满足对 ELF 文件的分析和调试的需求。
—— GPT-3.5

程序 = 指令 + 数据

假设有 a.c:

// import from global
extern const int eci;
extern int ei;

// static
static int si;
static const int sci;

// export
const int ci;
int i = 1;

int print(int);

int main() {
    print(eci);
    print(i);
    return 0;
}

注:

  1. C 语言中的 static 关键字是限制该符号仅在本文件内使用,是该文件内局部的(local)
  2. C 语言中的 extern 关键字表示使用外部定义的变量,可以认为是 import from global
  3. C 语言中在任何函数外定义变量如 int a 将会暴露给外部,可以认为是 export to global

一份文件编译好后应当包含如下信息:

  1. 文件内定义的函数,如 main,放在 .text
  2. 文件使用了外部定义的函数,如 print
  3. 文件使用了外部定义的变量,用 extern 关键字导入,如 eciei
  4. 文件内定义的变量,并且使用 static 关键字限定不导出,如 sisci
  5. 文件内定义的变量,并且导出给其他文件使用,也就是 global 变量,如 cii

毕竟 程序 = 代码 + 数据,CPU 就是指针在函数间跳来跳去修改数据。

符号表

gcc -c a.c -o a.o
objdump -t a.o
readelf -s a.o

objdump:

a.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 a.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l     O .bss   0000000000000004 si
0000000000000000 l     O .rodata        0000000000000004 sci
0000000000000004 g     O .rodata        0000000000000004 ci
0000000000000000 g     O .data  0000000000000004 i
0000000000000000 g     F .text  0000000000000029 main
0000000000000000         *UND*  0000000000000000 eci
0000000000000000         *UND*  0000000000000000 print

关于变量:
在文件内部定义的变量,每个变量都有 l 或者 g 标识,标识 Local 或者 Global。

  1. const 一定在 .rodata
  2. .bss 意思是 block started by symbol,用来存储 uninitialized 变量以节约空间,
  3. .data 就是存放初始化好的变量
  4. 被优化掉未使用的 extern 就不在符号表中,如 ei

UND 意思是,不在任何一个 section 节中

静态链接与重定位

不知道的地址先用 0 填充

在 assemble 得到的 二进制代码中,总会有一条 call print 的函数,但是这个 print 的地址还是未知的,所以先用 0 填充,
同时需要记录之后链接要将这个位置替换到真实的函数位置。

查看 a.c 汇编后的结果:

objdump -d a.o

a.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # e <main+0xe>
   e:   89 c7                   mov    %eax,%edi
  10:   e8 00 00 00 00          call   15 <main+0x15>
  15:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 1b <main+0x1b>
  1b:   89 c7                   mov    %eax,%edi
  1d:   e8 00 00 00 00          call   22 <main+0x22>
  22:   b8 00 00 00 00          mov    $0x0,%eax
  27:   5d                      pop    %rbp
  28:   c3                      ret

a.c 中我们调用了两次函数,print(i)print(eci)

  1. 10 和 1d 的 call print 指令都没有填写地址
  2. 调用的参数都是 0x0(%rip)

重定位表记录那些暂时用 0 填充的位置

这些地方肯定是要修改的,记录在a.o 的重定位表中:

objdump -r a.o

a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
000000000000000a R_X86_64_PC32     eci-0x0000000000000004
0000000000000011 R_X86_64_PLT32    print-0x0000000000000004
0000000000000017 R_X86_64_PC32     i-0x0000000000000004
000000000000001e R_X86_64_PLT32    print-0x0000000000000004



RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text

链接后修改位置

b.c

// b.c
#include <stdio.h>

int eci = 2;
int ei = 3;
void print(int val)
{
    printf("%d\n", val);
    return;
}

a.o 和 b.o 合并的时候,就把 text 合并,并且根据重定位表修改 a.o 中使用外部符号对应的 byte 位置

gcc -c b.c -o b.o
gcc a.o b.o -o ab.out
objdump -d ab.out # you can see the actual function and variable address

*.o 没有 segments

sections 详细记录了每个节的数据,用于给 linker(链接器)看,我们使用 a.ob.o 链接为 ab.out,所以这两个 .o 文件一定有 sections。
但是当使用命令 readelf -l a.o 时候,会提示「There are no program headers in this file.」。
因为 segments 是给 loader(加载器)看的,决定要把各个 sections 加载到内存的哪些地方,a.o 作为中间文件,加载器不会加载,所以没有 segments。

如果使用命令:readelf -l ab.out,解析可执行文件的 segments,会发现 segments 就是 sections 的集合,不同的 sections 可以被 map 到同一个 segment 中:

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000628 0x0000000000000628  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001ad 0x00000000000001ad  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x000000000000011c 0x000000000000011c  R      0x1000
  LOAD           0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000264 0x0000000000000270  RW     0x1000
  DYNAMIC        0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
                 0x00000000000001f0 0x00000000000001f0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x0000000000002010 0x0000000000002010 0x0000000000002010
                 0x000000000000003c 0x000000000000003c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000248 0x0000000000000248  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .plt.got .plt.sec .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11
   12     .init_array .fini_array .dynamic .got

动态链接

构造一个动态依赖库依赖于另一个动态依赖库的例子:a 无法直接访问 c 中的数据,只能通过 b 去访问。

gcc -fPIC -shared -o b.so b.c # compile b.c to shared object b.so
gcc -fPIC -shared -o c.so c.c
gcc -o abc a.c b.so c.so # compile and link to abc

// a.c
extern void print_in_b(int);
extern int get_var_in_c();

int main() {
    int a = get_var_in_c();
    print_in_b(a);
    return 0;
}
// b.c
extern int var_in_c;
extern void print_in_c(int);

void print_in_b(int val) {
    print_in_c(val);
}

int get_var_in_c() {
    return var_in_c;
}
// c.c
#include<stdio.h>
int var_in_c = 1;

void print_in_c (int val) {
    printf("%d\n", val);
}

根据依赖的顺序,需要把 c.so 中的函数加载到内存中,才可以调用 b.so 中的函数。
因为 c.so 可能在多个进程中,每个进程中 print_in_c 的地址不一定是一样的,
所以在 b.so 中 call print_in_c 的地址不能是写死的,要先把 c.so 加载到内存中,才能确定。

动态依赖库依赖外部变量

在 b.c 中访问了 c.c 中的变量:

extern int var_in_c;

int get_var_in_c() {
    return var_in_c;
}

对应的汇编如下:

objdump -d b.so

0000000000001135 <get_var_in_c>:
    1135:       f3 0f 1e fa             endbr64
    1139:       55                      push   %rbp
    113a:       48 89 e5                mov    %rsp,%rbp
    113d:       48 8b 05 ac 2e 00 00    mov    0x2eac(%rip),%rax        # 3ff0 <var_in_c>
    1144:       8b 00                   mov    (%rax),%eax
    1146:       5d                      pop    %rbp
    1147:       c3                      ret

GPT 说:

这段代码的逻辑很简单,它从 var_in_c 变量中获取值并返回。
首先,它将当前函数的基址 %rbp 压入栈中,然后将栈指针 %rsp 的值复制到 %rbp,建立当前函数的栈帧。
接下来,使用 mov 指令将 var_in_c 的地址加载到 %rax 寄存器中。
然后,使用 mov 指令将 %rax 寄存器中的值(即 var_in_c 的值)复制到 %eax 寄存器中。
最后,通过 pop 指令恢复上一个函数的栈帧基址,并使用 ret 指令返回到调用者。

也就是说,b.so 中有几个字节记录了 var_in_c 的地址,每次访问 var_in_c 时候,先找到这个地址,再取出来。
mov a,bmov (a),b 是两种不同的指令,后者是需要取地址的。
var_in_c 的地址就记录在 0x2eac(%rip) 中(当 %rip 指向 113d 的时候)。

.got 负责记录使用了外部变量的地址:

objdump -R b.so

b.so:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
0000000000003e48 R_X86_64_RELATIVE  *ABS*+0x0000000000001110
0000000000003e50 R_X86_64_RELATIVE  *ABS*+0x00000000000010d0
0000000000004020 R_X86_64_RELATIVE  *ABS*+0x0000000000004020
0000000000003fd8 R_X86_64_GLOB_DAT  __cxa_finalize
0000000000003fe0 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable
0000000000003fe8 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable
0000000000003ff0 R_X86_64_GLOB_DAT  var_in_c
0000000000003ff8 R_X86_64_GLOB_DAT  __gmon_start__
0000000000004018 R_X86_64_JUMP_SLOT  print_in_c

b.so 中,记录了 var_in_c 的 offset 为 0x3ff0 = 0x11dd + 0x2eac, 11dd 是 rip 下一条指令的地址,正好对上。

113d:       48 8b 05 ac 2e 00 00    mov    0x2eac(%rip),%rax        # 3ff0 <var_in_c>

动态链接库依赖外部函数

print_in_b 中对 print_in_c 的调用,也是通过指针记录 print_in_c 的地址,先 call print_in_c@plt,再 call print_in_c.


Disassembly of section .plt.sec:

0000000000001050 <print_in_c@plt>:
    1050:       f3 0f 1e fa             endbr64
    1054:       f2 ff 25 bd 2f 00 00    bnd jmp *0x2fbd(%rip)        # 4018 <print_in_c>
    105b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

0000000000001119 <print_in_b>:
    1119:       f3 0f 1e fa             endbr64
    111d:       55                      push   %rbp
    111e:       48 89 e5                mov    %rsp,%rbp
    1121:       48 83 ec 10             sub    $0x10,%rsp
    1125:       89 7d fc                mov    %edi,-0x4(%rbp)
    1128:       8b 45 fc                mov    -0x4(%rbp),%eax
    112b:       89 c7                   mov    %eax,%edi
    112d:       e8 1e ff ff ff          call   1050 <print_in_c@plt>
    1132:       90                      nop
    1133:       c9                      leave
    1134:       c3                      ret

动态链接库内部使用自己的地址

如果在 b.c 中加入:

int var_in_b = 1;
int* ptr_in_b = &var_in_b;

ptr_in_b 的值也是要在 b.so 转载到内存后才可以确定,这个记录在 rela.dyn 中[2]

延迟绑定

调用 print_in_c@plt 时候,使用的是 offset 为 0x4018 内的值,
但是因为使用了延迟绑定技术,这里现在还没放 print_in_c 的地址,而是指向 .plt 中一段解析出 print_in_c 的指令。

b.so 中,0x4018 存放的值是 0x1030:

objdump -s -j .got.plt b.so

b.so:     file format elf64-x86-64

Contents of section .got.plt:
 4000 583e0000 00000000 00000000 00000000  X>..............
 4010 00000000 00000000 30100000 00000000  ........0.......

第一次调用 print_in_c 的时候,会先跳转到 .plt 中执行解析出 print_in_c 的指令,也就是 0x1030:

Disassembly of section .plt:

0000000000001020 <.plt>:
    1020:       ff 35 e2 2f 00 00       push   0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:       f2 ff 25 e3 2f 00 00    bnd jmp *0x2fe3(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
    102d:       0f 1f 00                nopl   (%rax)
    1030:       f3 0f 1e fa             endbr64
    1034:       68 00 00 00 00          push   $0x0
    1039:       f2 e9 e1 ff ff ff       bnd jmp 1020 <_init+0x20>
    103f:       90                      nop

很遗憾,只能分析到这里了,因为 0x4010 处应该就是存放解析 print_in_c 地址的逻辑地址,也就是其他博客说的 dl_runtime_resolve[2:1]
但是 b.so 中 0x4010 还是用 0 填充,如果读者知道这个值是在什么时候被修改的,教教我好嘛。

总结

  1. .got:动态链接库使用了外部变量,要记录其地址
  2. .got.plt: 动态链接库使用了外部函数,要记录其地址
  3. .plt: .got.plt 中第一次先跳转到 .plt 中,调用 dl_runtime_resolve 解析地址
  4. .rela.plt: 解析地址需要使用 dl_runtime_resolve 的参数
  5. .rela.dyn: 动态链接库自己用了自己变量的地址

  1. Linux 系统中编译、链接的基石-ELF 文件:扒开它的层层外衣,从字节码的粒度来探索 - 知乎 ↩︎

  2. 一文看懂动态链接 - 知乎 ↩︎ ↩︎

标签:文件,00,int,objdump,ELF,64,print,so,链接
From: https://www.cnblogs.com/ticlab/p/18280144

相关文章

  • 在多线程并发操作中处理大量文件时,以下是一些关键的底层原理和技术:
    在多线程并发操作中处理大量文件时,以下是一些关键的底层原理和技术:1.文件句柄管理每个线程需要独立地管理文件句柄,文件句柄是操作系统提供的用于标识和访问文件的资源。在Windows环境下,使用CreateFile函数可以打开文件并获得文件句柄。每个文件句柄具有其自己的上下文和状态,......
  • 在Windows操作系统中,与文件系统进行交互主要通过一系列的API函数来实现,这些函数包括底
    操作文件系统API与操作系统的文件系统进行交互,涉及到底层的文件系统操作和文件属性管理。不同的操作系统提供了不同的API和机制来执行这些操作,但基本的原理和流程大致相似。文件系统API的基本操作1.文件时间戳(创建时间、修改时间、访问时间)创建时间(CreationTime):表示文件被创......
  • Day 2. Linux文件系统管理
    几个常见的处理目录的命令ls(英文全拼:listfiles):列出目录及文件名cd(英文全拼:changedirectory):切换目录pwd(英文全拼:printworkdirectory):显示目前的目录mkdir(英文全拼:makedirectory):创建一个新的目录rmdir(英文全拼:removedirectory):删除一个空的目录cp(英文全拼:copyfile):复......
  • C++编译问题,解决arm下链接静态库,引起的relocation R_AARCH64_ADR_PREL_PG_HI21 agains
    显示的完整错误如下:relocationR_AARCH64_ADR_PREL_PG_HI21againstsymbol`ZN2c43yml9free_implEPvmS1'whichmaybindexternallycannotbeusedwhenmakingasharedobject;recompilewith-fPIC根据提示,在链接.a静态库时,应该在编译时加上参数-fPIC然而CMake文件中已......
  • 修改文件夹的图标、颜色和其他外观特征可以通过修改注册表、编辑系统文件或者调用 Win
    修改文件夹的图标、颜色和其他外观特征通常涉及以下底层原理和方法:注册表修改:Windows中的文件夹外观特征通常保存在注册表中。通过修改特定的注册表项,可以实现更改文件夹的图标、颜色等外观。具体来说,文件夹的外观设置通常存储在注册表路径类似于 HKEY_CURRENT_USER\Softw......
  • 诺森德塔防游戏启动故障:msvcp110.dll文件缺失的高效解决策略
    《诺森德塔防》是一部以二战为背景的“肉鸽塔防”游戏,拥有着极为火爆的战场表现,让你能充分感受到收割成片敌人的快感,同时在玩法及策略性上都有着突出表现,然而最近很多用户都遇到了启动故障:msvcp110.dll文件缺失的问题,下面一起来看看解决方法介绍吧!重新安装MicrosoftVisualC......
  • .js.map文件泄露/Springboot信息泄露
    目录框架识别Webpack简述.js.map文件泄露利用Springboot 很多网站都使用的是现有的框架进行开发的,因此相当于很多目录和文件的路径都是开源可知的,因此我们就可以直接访问对应的路径,如果网站没有进行限制就有可能会导致敏感信息泄露框架识别可以根据页面的报错信息......
  • 更加优雅的下载文件 --- http header Content-Disposition 学习
    更加优雅的下载文件---httpheaderContent-Disposition学习在响应头中在请求头中a标签的download属性小结Content-Disposition在响应头中,告诉浏览器如何处理返回的内容,在表单提交中,说明表单字段信息。在响应头中用在响应头中,告诉浏览器如何处理返回的内容......
  • 搭建sftp并且保证普通用户有权限操作对应文件
    创建SFTP用户组为SFTP用户创建一个专用组:groupaddsftpusers创建SFTP用户创建SFTP用户splsz并将其添加到sftpusers组,同时指定用户的主目录和禁止shell访问:useradd-gsftpusers-s/sbin/nologinsplszpasswdsplsz创建目录结构sudomkdir-p/home/zhangqiang/davinci/......
  • IntelliJ IDEA java maven项目读取配置文件信息 java.util.ResourceBundle 方式
    一、在main目录下新建resources目录并将其设为资源文件目录  创建config.properties文件二、在pom.xml中添加下面代码 只这样打包后jar才能有配置文件<resources><resource><filtering>true</filtering><directory>src/main/......