二进制文件格式

本文探讨了printf函数在Linux中如何通过动态链接库 libc-2.33.so 实现,以及elf文件格式在程序执行中的关键作用,包括代码段、数据段、bss段和链接器的作用。通过反汇编和段表分析,揭示了程序的内部结构和链接机制。
摘要由CSDN通过智能技术生成

一.libc.so

相信很多人都知道编译一个c或者一个c++程序的时候都知道要经过预处理,编译,汇编,链接这四步操作。大家都知道printf这个函数在stdio.h 这个头文件中,原来我在windows里面的devc++看到了printf的定义,但是当我在linux里面打开stdio.h的时候并没有看到函数的定义,有的只是函数的声明,这大大激发了我对知识的渴望。后来我知道了printf这个函数是动态链接过来的。我们来看一下这段代码。

  1 #include <stdio.h>
  2          
  3 int main(void)
  4 {        
  5     printf("hello world");
  6     while (1)
  7         ;                                                                                                                                                                                
  8     return 0;
  9 }        

这段代码用了个printf函数。我们编译然后执行以下操作。

lonelyeagle@myarch ~ ./a.out&
[1] 3937
lonelyeagle@myarch  /proc/3937  cat  maps
56504cc3b000-56504cc3c000 r--p 00000000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3c000-56504cc3d000 r-xp 00001000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3d000-56504cc3e000 r--p 00002000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3e000-56504cc3f000 r--p 00002000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3f000-56504cc40000 rw-p 00003000 103:09 1836512                   /home/lonelyeagle/a.out
56504e481000-56504e4a2000 rw-p 00000000 00:00 0                          [heap]
7f5782648000-7f578264a000 rw-p 00000000 00:00 0 
7f578264a000-7f5782670000 r--p 00000000 103:08 1838455                   /usr/lib/libc-2.33.so
7f5782670000-7f57827bb000 r-xp 00026000 103:08 1838455                   /usr/lib/libc-2.33.so
7f57827bb000-7f5782807000 r--p 00171000 103:08 1838455                   /usr/lib/libc-2.33.so
7f5782807000-7f578280a000 r--p 001bc000 103:08 1838455                   /usr/lib/libc-2.33.so
7f578280a000-7f578280d000 rw-p 001bf000 103:08 1838455                   /usr/lib/libc-2.33.so
7f578280d000-7f5782818000 rw-p 00000000 00:00 0 
7f5782839000-7f578283a000 r--p 00000000 103:08 1838444                   /usr/lib/ld-2.33.so
7f578283a000-7f578285e000 r-xp 00001000 103:08 1838444                   /usr/lib/ld-2.33.so
7f578285e000-7f5782867000 r--p 00025000 103:08 1838444                   /usr/lib/ld-2.33.so
7f5782867000-7f5782869000 r--p 0002d000 103:08 1838444                   /usr/lib/ld-2.33.so
7f5782869000-7f578286b000 rw-p 0002f000 103:08 1838444                   /usr/lib/ld-2.33.so
7ffd24920000-7ffd24941000 rw-p 00000000 00:00 0                          [stack]
7ffd249c0000-7ffd249c4000 r--p 00000000 00:00 0                          [vvar]
7ffd249c4000-7ffd249c6000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

这样我们就可以看到整个进程的地址空间的文件的映射。其中的 /usr/lib/libc-2.33.so 就是我们c程序在linux上运行时需要的动态链接库。/usr/lib/ld-2.33.so就是linux下的动态连接器,所以printf这个函数从那里来这个问题的答案已经显而易见了。

二.elf文件的格式

这里不得不说的就是elf文件的格式。可执行文件,动态链接库,静态链接库文件都按照elf文件格式存储的,所以学习elf文件格式对我们理解一个程序非常的有用。
我们先来看看下面这段代码

    1 int printf(const char* format, ... );
    2      
    3 int global_init_var = 84;
    4 int global_uninit_var;
    5      
    6 void funcl(int i)
    7 {    
    8     printf("%d\n", i);
    9 }    
   10      
   11 int main(void)
   12 {    
   13     static int static_var = 85;
   14     static int static_var2;
   15     int a = 1;
   16     int b;                                                                                                                    
   17     funcl(static_var + static_var2 + a + b);
   18     return 0;
   19 }    

gcc -c 测试.c    这个命令只编译不链接

我们把这个目标文件的基本信息打印出来

lonelyeagle@myarch  ~/程序员的自我修养  objdump -h 测试.o 

测试.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000005c  0000000000000000  0000000000000000  00000040  2**0    
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE      实际存在
  1 .data         00000008  0000000000000000  0000000000000000  0000009c  2**2
                  CONTENTS, ALLOC, LOAD, DATA                       实际存在
  2 .bss          00000008  0000000000000000  0000000000000000  000000a4  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA            实际存在 
  4 .comment      00000013  0000000000000000  0000000000000000  000000a8  2**0
                  CONTENTS, READONLY                              实际存在
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000bb  2**0
                  CONTENTS, READONLY       但是size 为0 
  6 .note.gnu.property 00000030  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA   实际存在
  7 .eh_frame     00000058  0000000000000000  0000000000000000  000000f0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA   实际存在

不同的目标文件可以拥有不同数量及不同类型的“段”
程序源代码编译后的机器指令被放在代码段,代码段常见的名字有“.code",".text",已初始化全局变量和局部静态变量数据经常放在数据段,一般的名字都叫".data".未初始化的全局变量和局部静态变量一般放在一个叫"bss"的段里。
第2行中的contents ,alloc等表示段的属性。contents表示该段在文件中存在内容,.bss段没有contents,表示他实际上在elf文件中不存在内容。

我们对这个目标文件反汇编
下面的是目标文件中各个段的内容,有contents的就表示存在内容,所以只显示了下面几个段的内容。
最左边的是偏移量,中间的那些是内容,我们可以看到,一行分为4列,一列8个数字,也就是4个字节,所以一行16个字节。最右边是内容用ascll码翻译出来的内容。

lonelyeagle@myarch  ~/程序员的自我修养  objdump -s -d 测试.o 

测试.o:     文件格式 elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 488d0500 00000048 89c7b800 000000e8  H......H........
 0020 00000000 90c9c355 4889e548 83ec10c7  .......UH..H....
 0030 45f80100 00008b15 00000000 8b050000  E...............
 0040 000001c2 8b45f801 c28b45fc 01d089c7  .....E....E.....
 0050 e8000000 00b80000 0000c9c3           ............    
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .comment:
 0000 00474343 3a202847 4e552920 31312e31  .GCC: (GNU) 11.1
 0010 2e3000                               .0.             
Contents of section .note.gnu.property:
 0000 04000000 20000000 05000000 474e5500  .... .......GNU.
 0010 020001c0 04000000 00000000 00000000  ................
 0020 010001c0 04000000 01000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 27000000 00410e10 8602430d  ....'....A....C.
 0030 06620c07 08000000 1c000000 3c000000  .b..........<...
 0040 00000000 35000000 00410e10 8602430d  ....5....A....C.
 0050 06700c07 08000000                    .p......        

这个是反汇编的结果

Disassembly of section .text:

0000000000000000 <funcl>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   8b 45 fc                mov    -0x4(%rbp),%eax
   e:   89 c6                   mov    %eax,%esi
  10:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 17 <funcl+0x17>
  17:   48 89 c7                mov    %rax,%rdi
  1a:   b8 00 00 00 00          mov    $0x0,%eax
  1f:   e8 00 00 00 00          call   24 <funcl+0x24>
  24:   90                      nop
  25:   c9                      leave  
  26:   c3                      ret    

0000000000000027 <main>:
  27:   55                      push   %rbp
  28:   48 89 e5                mov    %rsp,%rbp
  2b:   48 83 ec 10             sub    $0x10,%rsp
  2f:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  36:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 3c <main+0x15>
  3c:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 42 <main+0x1b>
  42:   01 c2                   add    %eax,%edx
  44:   8b 45 f8                mov    -0x8(%rbp),%eax
  47:   01 c2                   add    %eax,%edx
  49:   8b 45 fc                mov    -0x4(%rbp),%eax
  4c:   01 d0                   add    %edx,%eax
  4e:   89 c7                   mov    %eax,%edi
  50:   e8 00 00 00 00          call   55 <main+0x2e>
  55:   b8 00 00 00 00          mov    $0x0,%eax
  5a:   c9                      leave  
  5b:   c3                      ret    
1.data段

.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。前面的代码里面一共有两个这样的变量,分别是global_init_varabal与static_var。这两个变量每个都是4字节,一共8个字节,所以.data这个段的大小是8字节
我们看到".data"段里的前4个字节,从低到高分别为 0x54,0x00,0x00,0x00,这个值刚好是global_init_varabal,即十进制的84。

2.rodata段

.rodata段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。对这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。printf函数里面用到了一个字符串常量,"%d\n",它是一种只读数据,所以他被放在了.rodata段。

3.bss段

.bss 段存放的是未初始化的全局变量和局部静态变量。上述代码中global_unint_var 和 static_var2 就是存放.bss段,更准确的说法是.bss 段为他们预留了空间。

4.文件头
 lonelyeagle@myarch  ~/程序员的自我修养  readelf -h 测试.o 
ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:              0x0
  程序头起点:              0 (bytes into file)
  Start of section headers:          1064 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13

文件头,它包含了描述整个文件的基本属性,比如elf文件版本,目标机器型号,程序入口地址等。
magic的16个字节用来标识ELF文件的平台属性。

5.段表
 lonelyeagle@myarch  ~/程序员的自我修养  readelf -S 测试.o
There are 14 section headers, starting at offset 0x428:

节头:
  [] 名称              类型(type)             地址              偏移量
       大小              全体大小          旗标(flg)   链接(lk)   信息(inf)   对齐(al)
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000005c  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000308
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000009c
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a4
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a4
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a8
       0000000000000013  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000bb
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  000000c0
       0000000000000030  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000f0
       0000000000000058  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000380
       0000000000000030  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  00000148
       0000000000000150  0000000000000018          12     8     8
  [12] .strtab           STRTAB           0000000000000000  00000298
       000000000000006f  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000003b0
       0000000000000074  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

段表就是保存这些段的基本属性的结构比如每个段的段名,段的长度,在文件中的偏移,读写权力及段的其他属性。elf文件的段结构就是由段表决定的,编译器,链接器,和装载器都是依靠段表来定位和访问各个段的属性的。
对于我们的代码来说,段表就是有14个元素的数组,elf段表的这个数组的第一个元素是无效的段描述符,他的类型是null,除此之外每个段描述符都对应一个段。也就是说我们的代码一共有13个段。另外,objdump -h 只是把elf文件中最关键的段显示了出来,而省略了其他辅助性的段。

1.段的类型相关常量
常量含义
NULL无效段0
PROGBITS程序段,代码段,数据段都是这种类型1
SYMTAB表示该段的内容为符号表2
STRTAB表示该段的内容为字符串表3
RELA重定位表,该段包含了重定位信息,这个段跟静态链接和动态链接有关4
HASH符号表的哈希表5
DYNAMIC动态链接信息6
NOTE提示性信息7
NOBITS表示该段在文件中没有内容,比如.BSS段8
REL包含了重定位信息9
SHLIB保留10
DNYSYM动态链接的符号表11
2. 段的标志位(也就是上面图里面的旗帜)

也可以参考图里面 Key to Flags 里面的内容。

WRITE表示该段在进程中可写
ALLOC表示这段在进程空间中需要分配空间,有些包含指示或控制信息的段不需要在进程空间中被分配空间,他们一般不会有这个标志,像代码段,数据段和.bss段都会有这个标志位
EXECINSTR表示该段在进程空间中可以被执行,一般指代码段
3.段的链接信息

如果段的类型是与链接相关的,比如重定位表,符号表等,那么sh_link和sh_info这两个成员包含的意义如下面的表所比示,对于其他的段,sh_link 和 sh_info 没有意义

typelink(lk)info(inf)
DYNAMIC该段使用的字符串在段表中的下标0
HASH该段所使用的符号表在段表中的下标0
REL 和 RELA该段所使用的相应符号表在段表中的下标该重定位表所作用的段在段表中的下标
6.重定位表

在测试.o中有一个.rel.text的段,他的类型为rela,也就是说他是一个重定位表,因为链接器在处理目标文件时,需要对目标文件中某些部件进行重定位,即代码段和数据段中那些绝对地址的引用的位置,这些重定位信息都记录在elf文件的重定位表里面,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表,比如.rel.text就是.text的重定位表,.text中有printf函数的调用。.data段中没有对绝对地址的引用,它只是包含了几个常量。所以没有.rel.data这个段。
一个重定位表,它的"link"表示符号表的下标,他的"info"表示他作用于那个段。比如,“.rel.text"作用于作用于".text"段,而".text"段的下标为"1",那么".rel.text"的"info"为1,在上图中,我们可以观察到符号表的下标是11。不只是.rel.text是这样,.rela.eh_frame的sh_link也是11。

7.符号表
 lonelyeagle@myarch  ~/程序员的自我修养  readelf  -s 测试.o
 
Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS ��.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_var2.0
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    10: 0000000000000000    39 FUNC    GLOBAL DEFAULT    1 funcl
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    13: 0000000000000027    53 FUNC    GLOBAL DEFAULT    1 main
1.符号绑定(Bind)
宏定义名说明
local0局部符号,对于目标文件的外表不可见
global1全局符号,外部可见
weak2弱引用

强符号与弱符号
我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。
对于c/c++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
注意:强符号和弱符号都是针对定义来说的,不是针对符号的引用。

    1 extern int ext;
    2      
    3 int weak;
    4 int strong = 1;
    5 __attribute__((week)) week2 = 2;   //弱引用
    6      
    7 int main()
    8 {    
    9     return 0;       

上面这段程序,"week"和"week2"是弱符号,因为没有初始化。"strong"和"main"是强符号。”ext"即非强符号也非弱符号,它是一个外部符号的引用。我们这里只是针对强符号和弱符号来说的。
三大规则
1.不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号,则链接器报符号重复定义错误。
2.如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
3.如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

  1 #include <stdio.h>
  2 int week;    // 弱引用
  3 int week = 10;    // 强引用
  4      
  5 int main(void)
  6 {    
  7     printf("%d\n",week);                                                                                                                                                                 
  8     return 0;
  9 }    
 lonelyeagle@myarch  ~  gcc 测试.c
 lonelyeagle@myarch  ~  ./a.out
10

弱引用和强引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,他们需要被正确决议。如果没有找到该符号的定义,链接器就会报符号为定义错误,这种被称为强引用与之对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议,如果该符号未被定义,则链接器对于该引用不报错。 链接器处理强引用和弱引用的过程几乎一样,只是对于未被定义的弱引用,链接器对于该引用不报错。弱符号和弱引用主要用户库的链接过程。 这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数,或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用,如果我们去掉了某些功能模块,那么程序也可以正常链接。

3.符号类型(Type)
宏定义名说明
NOTYPE0说明
OBJECT1该符号是一个数据对象,比如变量,数组
FUNC2该符号是一个函数或者其他可执行代码
SECTION3该符号表示一个段
FILE4该符号表示文件名
3.符号所在段(Ndx)

如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标。对于上图Num为0 的那个符号,永远都是一个为定义的符号。我们的printf函数因为没有被定义,所以他的Ndx是UND,意思是没有定义。

4.符号值

也就是value, 表示的是符号所对应的函数或者变量位于Ndx指定的段,偏移Value的位置。举个例子,因为main和func1都定义在代码里面,所以他们所在的位置都为代码段,所以Ndx的值为1,对应的段就是.text。这个printf函数,因为只是带代码里面被引用,没有被定义,所以他的Ndx为und,也就是没有被定义的意思。

三.总结

由于篇幅的原因,这里只说了主要的几个段,其实还有很多的东西可以讲,学习elf文件格式对于我们编程可能没有太直接的用处,但是对于提升我们对于一个程序的理解会有很大的提升。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值