二进制文件的内部是怎么样的?

二进制文件通常是与平台相关的,因此这里选择的Linux,ELF 二进制。

示例C代码:

#include <stdio.h>

int main() {
    printf("Penguin!\n");
}

编译 gcc -o hello hello.c 得到了hello的可执行文件。

-rwxr-xr-x 1 root root 16424 2月   4 14:11 hello
-rw-r--r-- 1 root root    61 2月   4 14:10 hello.c

原来的C文件只有61字节,编译后的可执行文件变成了16K左右,变大了很多。为了弄明白这个二进制文件的结构,我们需要补充一些相关知识, symbols, sections, and segments。

    symbols are like function names, and are used to answer “If I call printf and it’s defined somewhere else, how do I find it?”
    symbols are organized into sections – code lives in one section (.text), and data in another (.data, .rodata)
    sections are organized into segments
    ---
    译文
    符号表像函数名,当调用其它地方定义的函数(如上面的printf)时,需要直到可以怎么定位到它。
    符号表是按区组织的,代码位于一个区(.text), 数据位于另外的区(.data, .rodata)。
    区是按段组织的。

直接使用文本编辑器打开

直接用文本编辑器打开,除了很少一部分可以看得出的文本字符,其它尽是乱码。其中的ELF是文件格式的名称。

cat hello | head -c 3000
ELF>`@@▒8@8
           @@@@@@h▒▒@▒@@@PP@@▒▒  @ @@@.>@>@ (.(>@(>@▒▒▒@▒@DDP▒td  @ @44Q▒tdR▒td.>@>@▒▒/lib64/ld-linux-x86-64.so.2GNUס▒&▒▒6y▒J▒U▒▒:GNU
                                                       . libc.so.6puts__libc_start_mainGLIBC_2.2.5__gmon_start__ui     "▒?@@@ @@(@@

使用readelf来看符号表

readelf --symbols hello 
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    48: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5
    59: 0000000000400410     0 FUNC    GLOBAL DEFAULT   13 _start
    61: 00000000004004f4    16 FUNC    GLOBAL DEFAULT   13 main

在这里,我们看到了三个符号:main是main()函数的地址。puts看起来是对printf函数的引用,猜想应该是编译器为了优化将其更改为puts?。_start非常重要。当程序开始运行时,你可能认为它从main开始执行。但实际上它会转到_start。_start会执行一些非常重要的操作,其中包括调用main函数。

什么是符号

当你编写程序时,你可能会编写一个名为hello的函数。当你编译程序时,该函数的二进制代码将被标记为一个名为hello的符号。如果我从库中调用一个函数(比如printf),我们需要一种查找该函数代码的方法!从库中查找函数的过程称为链接。它可以在我们编译程序后进行(“静态链接”),也可以在我们运行程序时进行(“动态链接”)。
所以符号是使链接工作的关键!让我们来找一下printf的符号!它应该在libc库中,那里存放着所有的C标准库函数。
如果我在我的libc副本上运行nm命令,它会告诉我“没有符号”。但是互联网告诉我可以使用objdump -tT命令!这个命令有效!objdump -tT /lib/libc-2.17.so给我输出了如下内容。

objdump -tT /lib/libc-2.17.so | head -n 20

/lib/libc-2.17.so:     文件格式 elf32-i386

SYMBOL TABLE:
00000174 l    d  .note.gnu.build-id     00000000              .note.gnu.build-id
00000198 l    d  .note.ABI-tag  00000000              .note.ABI-tag
000001b8 l    d  .gnu.hash      00000000              .gnu.hash
00003f10 l    d  .dynsym        00000000              .dynsym
0000d590 l    d  .dynstr        00000000              .dynstr
0001347a l    d  .gnu.version   00000000              .gnu.version
0001474c l    d  .gnu.version_d 00000000              .gnu.version_d
00014bb4 l    d  .gnu.version_r 00000000              .gnu.version_r
00014bf4 l    d  .rel.dyn       00000000              .rel.dyn
00017634 l    d  .rel.plt       00000000              .rel.plt
00017680 l    d  .plt   00000000              .plt
00017720 l    d  .plt.got       00000000              .plt.got
00017730 l    d  .text  00000000              .text
00167260 l    d  __libc_freeres_fn      00000000              __libc_freeres_fn
00168f90 l    d  __libc_thread_freeres_fn       00000000              __libc_thread_freeres_fn
001692a0 l    d  .rodata        00000000              .rodata
......

如果你仔细观察,你会看到sprintf、strlen、fork、exec等你可能期望libc具有的函数。从这里,我们可以开始想象动态链接是如何工作的 - 我们看到hello调用了puts,然后我们可以在libc的符号表中查找puts的位置。

使用objdump查看二进制文件,并了解分区

objdump -s hello
Contents of section .text:
 400410 31ed4989 d15e4889 e24883e4 f0505449  1.I..^H..H...PTI
 400420 c7c0a005 400048c7 c1100540 0048c7c7  ....@.H....@.H..
 400430 f4044000 e8c7ffff fff49090 4883ec08  ..@.........H...
Contents of section .interp:
 400238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 400248 7838362d 36342e73 6f2e3200           x86-64.so.2.    
Contents of section .rodata:
 4005f8 01000200 50656e67 75696e21 00        ....Penguin!.  

    .text is the program’s actual code (the assembly). _start and main are both part of the .text section.
    .rodata is where some read-only data is stored (in this case, our string “Penguin!”)
    .interp is the filename of the dynamic linker!

分区和段之间的主要区别在于部分在链接时(由ld)使用,而段在执行时使用。objdump向我们显示了部分的内容,这很好,但是没有给我们足够关于部分的元数据,这让我感到遗憾。让我们尝试使用readelf代替:

readelf --sections hello
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [13] .text             PROGBITS         0000000000400410  00000410
       00000000000001d8  0000000000000000  AX       0     0     16
  [15] .rodata           PROGBITS         00000000004005f8  000005f8
       000000000000000b  0000000000000000   A       0     0     4
  [24] .data             PROGBITS         0000000000601010  00001010
       0000000000000010  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000601020  00001020
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .comment          PROGBITS         0000000000000000  00001020
       000000000000002a  0000000000000001  MS       0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

Neat! We can see .text is executable and read-only, .rodata (“read only data”) is read-only, and .data is read-write.

查看下汇编代码

在上面的 .text 区,我们看到的二进制的数字,比如下面的十六进制表示,这些表示什么?下面我们反编译看下汇编代码,

Contents of section .text:
 400410 31ed4989 d15e4889 e24883e4 f0505449  1.I..^H..H...PTI
 400420 c7c0a005 400048c7 c1100540 0048c7c7  ....@.H....@.H..
 400430 f4044000 e8c7ffff fff49090 4883ec08  ..@.........H...
objdump -d ./hello
Disassembly of section .text:

0000000000400410 <_start>:
  400410:       31 ed                   xor    %ebp,%ebp
  400412:       49 89 d1                mov    %rdx,%r9
  400415:       5e                      pop    %rsi
  400416:       48 89 e2                mov    %rsp,%rdx
  400419:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp

So we see that 31 ed is xoring two things. Neat! That’s all the assembly we’ll do for now.

最后,一个程序组织入段或者程序头,下面使用 readelf --segments hello来看看我们程序的段信息。

Program Headers:
  [... removed ...]
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006d4 0x00000000000006d4  R E    200000
  LOAD           0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001f8 0x0000000000000208  RW     200000
  [... removed ...]

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym
       .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt
       .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .ctors .dtors .jcr .dynamic .got 

段是用来指定如何将程序的各个部分如何放入到内存中,第一个LOAD段被标注为可读可执行,第二个为可读写。 .text is in the first segment (we want to read it but never write to it), and .data, .bss are in the second (we need to write to them, but not execute them).

可执行文件并不是什么魔法. ELF 像其它文件格式一样! 你可以使用 readelf, nm, 和 objdump 等工具来对linux二进制文件一探究竟


本文主要参考并译自 Julia Evans 的博文。
How is a binary executable organized? Let’s explore it!

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值