41、Linux 可执行文件格式与调试指南

Linux 可执行文件格式与调试指南

在 Linux 环境中,可执行文件有着独特的格式和调试方法。了解这些知识对于开发者和安全研究人员来说至关重要。本文将深入介绍 Linux 中的可执行文件格式,特别是 Executable and Linkable Format(ELF),以及如何使用相关工具进行调试。

1. 可执行文件格式概述

在早期的 C 语言开发中,Unix 系统上的可执行文件采用 a.out 格式,即汇编器输出格式。即使在今天,如果在使用 C 编译器时不指定输出文件名,仍可能得到名为 a.out 的文件。然而,现在常见的可执行文件格式是 ELF。

以下是一个使用 C++ 编译器编译示例程序生成的 a.out 文件的示例:

┌──(kilroy@badmilo)-[~]
└─$ ls a.out
a.out

┌──(kilroy@badmilo)-[~]
└─$ file a.out
a.out: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), 
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, 
BuildID[sha1]=19931f9e541de129129aac6ca74c833e5da30950, for GNU/Linux 
3.7.0, 
not stripped

从这个示例可以看出,虽然文件名为 a.out,但实际上它是一个 ELF 二进制文件。在 Linux 中,二进制文件是程序的常见称呼。

2. ELF 文件格式详解

ELF 是在 20 世纪 80 年代后期为 Unix 系统开发的文件格式,它类似于 PE 文件格式,是可执行文件的容器。ELF 文件有一个文件头,包含了关于文件的元数据,如程序应该运行的处理器和目标操作系统。

由于 ELF 是一种通用的文件格式,它被用于多种操作系统和硬件架构。但即使在不同的系统上找到相同的 ELF 文件,也不能直接执行,因为每个操作系统有不同的应用二进制接口(ABI)。

2.1 ABI 的影响

ABI 定义了程序与操作系统交互的常见方式,包括系统调用的定义、参数传递的约定等。不同的操作系统(甚至同一操作系统的不同版本)的 ABI 可能不同,因此即使文件格式相同,也不能在不同系统上直接执行 ELF 文件。

2.2 ELF 文件头结构

ELF 文件头包含了帮助加载器将程序加载到内存的信息。它支持 32 位和 64 位可执行文件,文件头的偏移量会根据内存位置的存储方式略有不同。

以下是 ELF 文件头的主要内容:
1. 魔数(Magic Number) :用于标识文件为 ELF 文件,其值为 0x7f454c46。
2. 系统位数和字节序 :定义是 32 位还是 64 位系统,以及目标系统是小端序还是大端序。
3. ELF 版本 :当前使用的是原始版本 1。
4. 目标操作系统 :指示文件的目标操作系统。
5. ABI 规范 :可能需要的 ABI 规范。
6. 文件类型 :告诉加载器文件是可执行文件还是库。
7. 目标处理器架构 :由于 ELF 已经存在了 30 多年,它可以支持相当多的处理器。
8. 入口点地址 :可执行文件的入口点地址。
9. 程序头地址 :程序头应紧跟文件头。
10. 节头地址 :指示可执行文件的不同节。

2.3 程序头的作用

程序头告诉加载器如何在内存中构建程序映像。它包含了程序不同段的信息,程序头表可能有多个条目,每个条目可以标记为读、写或执行。这有助于保护 ELF 可执行文件免受缓冲区溢出的影响,因为内存段应标记为是否可执行,栈段通常不应可执行。

3. 使用 readelf 工具分析 ELF 文件

Linux 系统(包括 Kali Linux)通常包含 readelf 工具,它可以提供关于 ELF 文件结构和内容的信息。

以下是使用 readelf 打印 ELF 头的示例:

┌──(kilroy@badmilo)-[~]
└─$ readelf -h sample
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent 
Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1070
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14608 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

使用 readelf 时,可以在命令行提供不同的开关来获取 ELF 文件的不同信息。除了 ELF 头,还可以使用 readelf -l 列出程序头,如下所示:

┌──(kilroy@badmilo)-[~]
└─$ readelf -l sample

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1070
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
                 0x0000000000000800 0x0000000000000800  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 
0x0000000000001000
                 0x00000000000001d1 0x00000000000001d1  R E
 <--snip-->
  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 .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .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
4. 符号表的作用

处理器通常期望通过内存地址来查找信息,但有时会使用名称代替。这就需要符号表,它将这些名称映射到内存地址。可以使用 readelf 来获取符号表的内容。

以下是一个符号表的示例:

┌──(kilroy@badmilo)-[~]
└─$ readelf -s sample

Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
[...]@GLIBCXX_3.4 (3)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
_[...]@GLIBC_2.34 (4)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
[...]@GLIBCXX_3.4 (3)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
[...]@GLIBCXX_3.4 (3)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
[...]@GLIBCXX_3.4.32 (5)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND 
[...]@GLIBCXX_3.4 (3)
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND 
_ITM_deregisterT[...]
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND 
__gmon_start__
     9: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND 
_ITM_registerTMC[...]
    10: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND 
[...]@GLIBC_2.2.5 (2)
    11: 0000000000004040   272 OBJECT  GLOBAL DEFAULT   26 
[...]@GLIBCXX_3.4 (3)
5. objdump 工具的使用

除了 readelf,还可以使用 objdump 工具获取类似的信息,但 readelf 更详细。

以下是 objdump 的输出示例:

┌──(kilroy@badmilo)-[~]
└─$ objdump -f sample

sample:     file format elf64-little
architecture: UNKNOWN!, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000000000001070
6. 调试 ELF 可执行文件

到目前为止,我们看到的都是静态信息,即文件中存储的内容。要查看动态信息,可以通过执行程序来实现,最好在受控的环境中使用专门的软件来管理执行。

6.1 调试的概念

调试是指使用软件在程序执行期间动态评估程序的过程。调试器是一种软件,它可以在用户提供的约束条件下执行程序。使用调试器,我们可以像正常运行一样执行程序,也可以设置断点并逐行执行程序。此外,我们还可以使用调试器查看程序的内存,根据执行状态获取数据。

6.2 Linux 中的主要调试器 - gdb

Linux 中主要使用的调试器是 gdb,即 GNU 调试器。在 Kali 系统中,gdb 可能不是默认安装的,可以通过运行 sudo apt install gdb 来安装。

调试程序是一项需要时间来掌握的技能,特别是使用功能丰富的 gdb 调试器。即使使用 GUI 调试器(如 ddd,它是 gdb 的 GUI 前端),也需要一些时间来适应运行程序和检查运行程序中的数据。程序越复杂,可使用的功能就越多,调试的复杂度也会增加。

6.3 编译带有调试符号的程序

为了充分利用调试器,程序应该在编译时包含调试符号。这样可以帮助调试器提供更多的信息,我们可以从可执行文件中引用源代码。当需要设置断点时,可以根据源代码来指定断点位置。如果程序崩溃,我们可以得到源代码中相应行的引用。但需要注意的是,引入程序的库(包括标准 C 库函数)不会提供额外的详细信息。

6.4 使用 gdb 调试程序的步骤
  1. 编译带有调试符号的程序 :在编译程序时,添加 -g 选项。例如: gcc -g sample.cpp -o sample
  2. 启动 gdb :在命令行中运行 gdb sample
  3. 设置断点 :可以根据函数名、行号和源文件来设置断点。例如: (gdb) break main
  4. 运行程序 :使用 run 命令启动程序。例如: (gdb) run
  5. 逐行执行程序 :使用 step next 命令逐行执行程序。 step 会进入调用的函数,而 next 会跳过函数内部的执行。
  6. 继续执行程序 :如果不想逐行执行,可以使用 continue 命令继续执行程序。
  7. 查看栈信息 :使用 frame bt 命令查看栈帧和调用栈信息。
  8. 查看变量内容 :使用 print 命令查看变量的内容。例如: (gdb) print strCpy::local

以下是一个设置断点和逐行执行程序的示例:

(gdb) break main
Breakpoint 1 at 0x117c: file sample.cpp, line 14.
(gdb) run
Starting program: /home/kilroy/sample
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-
gnu/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffe298) at sample.cpp:14
14  
    int val = 0;
(gdb) step
16  
    cout << "The value requested is " << addMe(915, 342) << 
endl;
(gdb) step
addMe (x=915, y=342) at sample.cpp:8
8   
    return x + y;
(gdb) continue
Continuing.
The value requested is 1257
[Inferior 1 (process 313571) exited normally]
6.5 使用 GUI 调试器 - ddd

ddd 是 gdb 的 GUI 前端,它通过点击按钮来调用 gdb。使用 ddd 的一个优点是,如果文件在当前目录中且包含调试符号,我们可以看到源代码。

在 ddd 中,我们可以通过选择函数或行并点击屏幕顶部的“Breakpoint”按钮来轻松设置断点。但需要注意的是,使用 GUI 调试器并不意味着可以在不理解调试原理的情况下进行调试,仍然需要花费时间来掌握调试技能并了解调试器提供的所有信息。GUI 调试器可以在屏幕上同时显示大量信息,而不需要依次运行多个命令并滚动查看输出。

通过以上内容,我们对 Linux 中的 ELF 可执行文件格式和调试方法有了更深入的了解。掌握这些知识可以帮助我们更好地开发和调试程序,提高程序的质量和安全性。

Linux 可执行文件格式与调试指南

7. 调试中遇到的问题及解决方法

在使用调试器调试程序时,可能会遇到一些常见问题,下面为大家详细介绍这些问题及相应的解决办法。
|问题|原因|解决方法|
| ---- | ---- | ---- |
|找不到调试符号|程序编译时未添加 -g 选项|重新编译程序,添加 -g 选项,如 gcc -g sample.cpp -o sample |
|无法进入库函数|库函数没有源代码|只能获取库函数执行位置的指示信息,无法查看源代码|
|程序崩溃后难以定位问题|未包含调试符号,无法获取源代码引用|编译时添加 -g 选项,使程序包含调试符号|

8. 调试流程总结

为了让大家更清晰地了解调试的整个流程,下面用 mermaid 流程图展示:

graph TD
    A[编译程序(添加 -g 选项)] --> B[启动 gdb(gdb sample)]
    B --> C[设置断点(break main 等)]
    C --> D[运行程序(run)]
    D --> E{是否到达断点}
    E -- 是 --> F[逐行执行(step 或 next)]
    E -- 否 --> D
    F --> G{是否继续逐行执行}
    G -- 是 --> F
    G -- 否 --> H[继续执行(continue)]
    H --> I{程序是否崩溃}
    I -- 是 --> J[查看栈信息(frame 和 bt)及变量内容(print)]
    I -- 否 --> K[程序正常结束]
9. 不同调试方式对比

在调试 ELF 可执行文件时,有命令行调试和 GUI 调试两种方式,下面通过表格对比它们的优缺点:
|调试方式|优点|缺点|
| ---- | ---- | ---- |
|命令行调试(gdb)|功能强大,可精确控制调试过程|需要记忆大量命令,操作相对复杂|
|GUI 调试(ddd)|操作直观,可同时查看大量信息,能显示源代码|依赖图形界面,可能在某些环境下无法使用,且仍需理解调试原理|

10. 进一步学习建议

如果你想更深入地掌握 Linux 可执行文件的调试技术,可以从以下几个方面入手:
1. 深入学习 gdb :gdb 功能丰富,有许多高级特性等待挖掘,如条件断点、数据断点等。可以通过阅读官方文档和相关教程来学习。
2. 研究不同类型程序的调试 :例如多线程程序、网络程序等,这些程序的调试有其独特之处,需要专门的技巧和方法。
3. 参与开源项目调试 :通过参与开源项目的调试工作,不仅可以提高自己的调试能力,还能学习到其他开发者的优秀经验。

总之,掌握 Linux 中 ELF 可执行文件的格式和调试方法,对于开发者和安全研究人员来说是非常有价值的。通过不断学习和实践,我们可以更加熟练地运用这些工具和技术,提高程序开发和调试的效率。希望本文能为你在 Linux 开发和调试的道路上提供一些帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值