GNU Binutils
一、ELF 文件
在介绍 GNU Binutils 前,先来对 ELF 文件做个简单的了解,因为后续所有操作都将围绕 ELF 文件展开。
1.1 ELF的定义
ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的 ELF 格式文件包括:
- 可执行文件(.exe)
- 可重定位文件(.o)
- 共享目标文件(.so)
- 核心转储文件(core)
1.2 查看文件是否为 ELF 文件
我们可以通过 file
命令来查看文件的类型。
$file libcalc.so
libcalc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=425ab595dac234d722130ce98a70d84d4bb55949, not stripped
- ELF
- dynamicall linked
- not stripped
$file libcalc.a
libcalc.a: current ar archive
二、GNU Binutils
GNU Binutils 是一组二进制工具集。主要包括:
- ld:GNU链接器
- as:GNU汇编器
- addr2line:把地址转化为文件名和行号
- nm:列出目标文件的符号列表
- objdump:显示目标文件信息
- readelf:显示elf格式的文件信息
- objcopy:拷贝部分section以生成新的可执行文件,如elf->hex、elf->bin等
- ar:创建、修改、解压一个静态库文件
- size:显示目标文件的节大小
- strings:从目标文件中列出可打印的字符串
- c++filt:过滤c++符号为可识别的c符号
- ranlib:生成库文件中的.o文件索引
- strip:丢弃程序文件中的符号信息
本篇文章就 addr2line、readelf、objdump、nm、objcopy 指令做个简单介绍。
在介绍之前先准备好所需的测试程序。
2.1 测试程序
2.1.1 testh
#ifndef __CALC_H__
#define __CALC_H__
int CALC_MULT(int a, int b);
int CALC_DIVI(int a, int b);
#endif
2.1.2 test.c
#include <stdio.h>
#include <math.h>
#include "calc.h"
/* 定义两个全局变量 */
int giNum1;
static int giNum2;
/* 定义一个静态函数 */
static int CALC_SQRT(int a)
{
return sqrt(a);
}
/* 定义两个非静态函数 */
int CALC_MULT(int a, int b)
{
return a * b;
}
int CALC_DIVI(int a, int b)
{
/* 此处缺少对 b 的判 0 操作, 用来构造 coredump */
return a / b;
}
2.1.3 Makefile
CC := gcc
OBJ := calc.o
CFLAGS := -fPIC -g
TARGET := libcalc.so
$(TARGET) : $(OBJ)
$(CC) -shared $< -o $@
%.o : %.c
$(CC) -c $< -o $@ $(CFLAGS)
clean :
rm -f $(TARGET)
rm -f $(OBJ)
.PHONY : clean
2.1.4 Tutorial
$make clean
$make # 将会在当前目录下生成 libcalc.so
2.2 addr2line
addr2line translates addresses into file names and line numbers. Given an address in an executable or an offset in a section of a relocatable object, it uses the debugging information to figure out which file name and line number are associated with it.
addr2line 将地址转换为文件名和行号。给定可执行文件中的地址或可重定位对象部分中的偏移量,它会使用调试信息来确定与之相关的文件名和行数。
常用指令格式:addr2line -pfe libcalc.so [addr]
选项 | 简写 | 描述 |
---|---|---|
--exe | -e | 设置输入文件名称(默认为 a.out) |
--functions | -f | 显示函数名 |
--pretty-print | -p | 优化输出方式 |
--basenames | -s | 去除目录名 |
2.3 readelf
readelf 命令可以查看 ELF 文件的详细信息。
选项 | 简写 | 功能 |
---|---|---|
--file-header | -h | Display the ELF file header |
--syms | -s | Display the symbol table |
--file-header
$readelf --file-header libcalc.so
ELF Header:
...
Type: DYN (Shared object file)
...
--syms
$readelf --syms libcalc.so
Symbol table '.dynsym' contains 15 entries: ---> 仅显示 GLOBAL 属性的内容
Num: Value Size Type Bind Vis Ndx Name
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sqrt
7: 0000000000201038 4 OBJECT GLOBAL DEFAULT 22 giNum1
9: 0000000000000710 19 FUNC GLOBAL DEFAULT 11 CALC_MULT
12: 0000000000000723 19 FUNC GLOBAL DEFAULT 11 CALC_DIVI
Symbol table '.symtab' contains 64 entries: ---> 显示所有的
Num: Value Size Type Bind Vis Ndx Name
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS calc.c
39: 0000000000201034 4 OBJECT LOCAL DEFAULT 22 giNum2
40: 00000000000006f5 27 FUNC LOCAL DEFAULT 11 CALC_SQRT
51: 0000000000201038 4 OBJECT GLOBAL DEFAULT 22 giNum1
55: 0000000000000710 19 FUNC GLOBAL DEFAULT 11 CALC_MULT
59: 0000000000000723 19 FUNC GLOBAL DEFAULT 11 CALC_DIVI
60: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sqrt
-
Value:符号在库中的偏移量,可以和 addr2line 搭配使用
-
Size:十进制表示,代表当前符号的大小(Value + Size = 下一个符号的 Value)。以 CALC_SQRT 为例:
# 查看 CALC_ADD 对应代码中的位置 $addr2line -e libcalc.so 6f5 calc.c:11 ---> CALC_SQRT # 偏移 27 后的位置 0x710(= 0x6f5 + 27) $addr2line -e libcalc.so 710 calc.c:17 ---> CALC_MULT # 再偏移 19 后的位置 0x723(= 0x710 + 19) $addr2line -e libcalc.so 723 calc.c:22 ---> CALC_DIVI
-
Type:符号类型
宏定义名 值 说明 举例 STT_NOTTYPE 0 未知类型符号 sqrt STT_OBJECT 1 该符号是个数据对象,如变量、数组等 giNum1
giNum2STT_FUNC 2 该符号是个函数或其他可执行代码 CALC_* STT_SECTION 3 该符号表示一个段,这种符号必须是 STB_LOCAL 的 - STT_FILE 4 该符号表示文件名,一般都是该目标文件所对应的原文件名
它一定是 STB_LOCAL 类型的,并且它的 st_shndx 一定是 SHN_ABScalc.c -
Bind:绑定信息
名称 说明 举例 LOCAL 局部符号,对目标文件外部不可见 giNum2
CALC_SQRTGLOBAL 全局符号,外部可见 giNum1
CALC_MULT
CALC_DIVIWEAK 弱引用 - -
Ndx:表示为符号所在的段
名称 说明 举例 ABS 表示该符号包含一个绝对值 - COMMON 一般表示为初始化的全局符号 - UNDEF 表示该符号未定义
在本目标文件中被引用但是定义在其他目标文件中sqrt
2.4 objdump
objdump 是一个 GCC 工具:
- 可以查看目标文件或者可执行文件的各个段信息。
- 可以显示有关文件的大量不同信息。
- 也可以显示符号,并且在联机帮助页中明确指出「这与 nm 程序提供的信息类似,但显示格式不同」
选项 | 简写 | 功能 |
---|---|---|
--archive-headers | -a | 显示档案头信息,展示档案每一个成员的文件格式。 |
--disassemble | -d | 反汇编目标文件,将机器指令反汇编成汇编代码。 |
--disassemble-all | -D | 与 -d 类似,但反汇编所有段(section) |
--syms | -t | 显示文件的符号表入口。 类似于 nm -s 提供的信息 |
--dynamic-syms | -T | 显示文件的动态符号表入口,仅仅对动态目标文件意义,比如某些共享库。 它显示的信息类似于 nm -D 显示的信息 |
# 常用命令
$objdump -Tt libcalc.so
libcalc.so: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 calc.c
0000000000201034 l O .bss 0000000000000004 giNum2
00000000000006f5 l F .text 000000000000001b CALC_SQRT
0000000000201038 g O .bss 0000000000000004 giNum1
0000000000000710 g F .text 0000000000000013 CALC_MULT
0000000000000723 g F .text 0000000000000013 CALC_DIVI
0000000000000000 *UND* 0000000000000000 sqrt
DYNAMIC SYMBOL TABLE:
0000000000000000 D *UND* 0000000000000000 sqrt
0000000000201038 g DO .bss 0000000000000004 Base giNum1
0000000000000710 g DF .text 0000000000000013 Base CALC_MULT
0000000000000723 g DF .text 0000000000000013 Base CALC_DIVI
- 第一列代表符号的偏移量,等价于 readelf 的 Value
- 第二列代表符号属性,local(l) 或者 global(g)
- 第三列的 DF(Dynamic Function),可以理解为对外暴露的接口
- 第五列代表符号的大小,十六进制表示,其值等价于 readelf 的 Size
2.5 nm
nm 是 names 的缩写,nm 命令主要是用来列出某些文件中的符号。
nm 命令的输出包含三个部分:
-
符号偏移量,默认显示十六进制,也可以指定
-
符号类型,小写表示是本地符号,大写表示全局符号(external)
符号类型 含义 b / B 符号位于未初始化数据段
用 0 初始化或未初始化d / D 符号已初始化 t / T 代码段的符号 U 未定以的符号 -
符号名称
选项 | 简写 | 说明 | 备注 |
---|---|---|---|
--dynamic | -D | 仅显示动态符号 | 作用于 libcalc.so |
--extern-only | -g | 仅显示外部符号 | 作用于 libcalc.so.sym |
--print-size | -S | 显示符号大小 | - |
--numeric-sort | -n | 按地址对符号进行升序排序 | - |
--reverse-sort | -r | 倒序输出 | - |
/* 全局变量, 在 nm 的执行结果中以大写字母显示 */
int nm_g_int; /* B */
int nm_g_int_1 = 0; /* B */
int nm_g_ini_2 = 1; /* D */
void nm_func() /* T */
{
print("nm_func");
return;
}
/* 用 static 修饰的, 在 nm 的执行结果中以小写字母显示 */
static int nm_gs_int; /* b */
static int nm_gs_int_1 = 0; /* b */
static int nm_gs_ini_2 = 1; /* d */
static void nm_s_func() /* t */
{
puts("nm_s_func");
return;
}
将上边的代码编译成一个动态库(如 libtest.so)后执行如下的 nm 指令查看执行结果:
#
$nm -nS libtest.so | grep -E "nm_|print|puts"
U print
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
0000000000000885 0000000000000018 T nm_func
000000000000089d 0000000000000013 t nm_s_func
0000000000201040 0000000000000004 D nm_g_ini_2
0000000000201044 0000000000000004 d nm_gs_ini_2
000000000020104c 0000000000000004 B nm_g_int_1
000000000020105c 0000000000000004 b nm_gs_int
0000000000201060 0000000000000004 b nm_gs_int_1
0000000000201088 0000000000000004 B nm_g_int
# 如果添加 -D 选项, 则只显示动态符号, 输出结果中的表现是全大写
$nm -nSD libtest.so | grep -E "nm_|print|puts"
U print
U printf
U puts
0000000000000885 0000000000000018 T nm_func
0000000000201040 0000000000000004 D nm_g_ini_2
000000000020104c 0000000000000004 B nm_g_int_1
0000000000201088 0000000000000004 B nm_g_int
将 libtest.so 进行符号分离 ---> libtest.so.sym:
$objcopy --only-keep-debug libtest.so libtest.so.sym
$nm -nsD libtest.so.sym
nm: libtest.so.sym: no symbols
$nm -nSg libtest.so.sym | grep -E "nm_|print|puts"
U print
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
0000000000000885 0000000000000018 T nm_func
0000000000201040 0000000000000004 D nm_g_ini_2
000000000020104c 0000000000000004 B nm_g_int_1
0000000000201088 0000000000000004 B nm_g_int
2.6 objcopy
objcopy 命令是 GNU Binutils 工具集中的一个命令行工具,用于复制目标文件中的某些内容到另一个文件中,或者修改目标文件的格式、结构和内容。objcopy 命令通常用于处理目标文件,包括可执行文件、共享库、静态库等等。
最常用的一个功能是将 ELF 文件(如动态库)的调试信息单独 dump 出来作为一个独立的 ELF 文件。
命令格式:objcopy [选项] 源文件 [目标文件]
如果没有 [目标文件],则操作结果将直接作用于 源文件 上。
选项 | 功能 |
---|---|
--strip-all | 删除目标文件中的所有符号信息 |
--strip-debug | 删除目标文件中的调试符号信息 |
--only-keep-debug | 将 ELF 文件的调试信息单独 dump 出来作为一个独立的 ELF 文件 |
2.7 strip
strip - Discard symbols from object files.
简单的说就是给文件脱掉外衣,具体就是从特定文件中剥掉一些符号信息和调试信息,使文件变小。
$strip libcalc.so
$strip --only-keep-debug libcalc.so
2.8 分离符号表
结合 objcopy 和 strip 完成分离操作:
# 1.导出
$objcopy --only-keep-debug libcalc.so libcalc.so.sym
# 2.删除符号信息
$strip libcalc.so
其实到这就已经完成 libcalc.so 的符号导出操作了:
- 对外发布使用 strip 后的 libcalc.so
- 问题定位使用 libcalc.so.sym
下面研究个比较有意思的,对比下 objcopy 和 strip 的执行结果。
首先用 file 查看文件类型:
$file libcalc.so
libcalc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, not stripped
关键字:not stripped
并记录下当前的库大小以及 md5 值:
md5 | size(单位:字节) |
---|---|
9160b9ed73edcadf65371d00e149bd96 | 9440 |
使用 obdcopy 操作 libcalc.so:
option | md5 | size(单位:字节) |
---|---|---|
objcopy --strip-all libcalc.so | c986763e6232035487754eda7d202c73 | 6016 |
objcopy --only-keep-debug libcalc.so | 240858401983ad61dbcd5025effa2bae | 5784 |
使用 strip 操作 libcalc.so:
option | md5 | size(单位:字节) |
---|---|---|
strip libcalc.so | c986763e6232035487754eda7d202c73 | 6016 |
strip --only-keep-debug | 240858401983ad61dbcd5025effa2bae | 5784 |
经过对比 md5 值可以看出,objcopy 的功能更加强大,如果不使用 [目标文件] 这一参数,起作用和 strip 保持一致。
但是 strip 的优点在于命令简洁。
查看操作后的文件类型:
$file libcalc.so
libcalc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, stripped
$file libcalc.so.sym
libcalc.so.sym: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, not stripped
关键字:libcalc.so --> stripped,libcalc.so.sym --> not stripped
三、如何判断 .so 和 .so.sym 是否配套
可以通过 file 命令确认是否配套。还是以 libcalc.so 为例。
3.1 对比实验
strip 前的 file 信息打印:
$file libcalc.so
libcalc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, not stripped
- BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae
剥离符号表 libcalc.so.sym 的 file 信息打印:
$objcopy --only-keep-debug libcalc.so libcalc.so.sym
$file libcalc.so.sym
libcalc.so.sym: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, not stripped
- BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae
strip 后的 file 信息打印:
$strip libcalc.so
$file libcalc.so
libcalc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae, stripped
- BuildID[sha1]=dbc1595b9564197d481159dd96f1ea06bcbe67ae
一目了然。
3.2 file 输出字段解释
- ELF 64-bit LSB shared object
- ELF 表示可执行和可链接格式(Executable and Linkable Format),是一种用于可执行文件、目标文件和共享库的标准文件格式;
- 64-bit LSB 表示该可执行文件是 64 位的,并且使用了「Least Significant Byte」顺序。
- version 1 (SYSV)
- version 1 表示 ELF 文件的版本;
- (SYSV) 表示它遵循 System V ABI(Application Binary Interface)标准。
- dynamically linked
- 表明这个可执行文件使用了动态链接(dynamically linked)这意味着它依赖于系统上已安装的共享库,在运行时才会解析和加载这些库。
- BuildID[sha1]=
- 提供了构建标识符,用于唯一标识二进制文件的构建,这对于调试和版本控制很有用。
- not stripped / stripped
- 表示该可执行文件为被剥离 / 已经被剥离(stripped);
- 被 stripped 意味着被去除了调试信息,这有助于减小文件大小,但使得调试更加困难。
有理有据!
3.3 遗留问题
如果使用同一套代码并且是同一个 gcc 环境不同时间(间隔一天)编译出来的两个同名的库 libcalc.so,其符号表是否可以共享?
目前看应该是可以的,因为对比了下两个库的 readelf,符号的偏移量保持一致,待实际验证。
之所以备注间隔一天,是因为我输出昨天编译的 libcalc.so 的 md5 值和 BuildID,值为:
md5 | BuildID |
---|---|
9160b9ed73edcadf65371d00e149bd96 | dbc1595b9564197d481159dd96f1ea06bcbe67ae |
今天重新 make clean ; make
若干次后,md5 和 BuildID 值均为:
md5 | BuildID |
---|---|
dcd6676d32d73e311e13a2626cd5db5d | c70e84b4cd868dace68673cebde6889243827e95 |
虽然 md5 值和 BuildID 有所变化,但是对比两个库的 readelf,符号的偏移量却保持一致:
# 昨天的库的执行结果
$md5sum libcalc.so
9160b9ed73edcadf65371d00e149bd96 libcalc.so
$readelf --syms libcalc.so | grep -E "giNum1|giNum2|CALC_MULT|CALC_DIVI"
39: 0000000000201034 4 OBJECT LOCAL DEFAULT 22 giNum2
51: 0000000000201038 4 OBJECT GLOBAL DEFAULT 22 giNum1
55: 0000000000000710 19 FUNC GLOBAL DEFAULT 11 CALC_MULT
59: 0000000000000723 19 FUNC GLOBAL DEFAULT 11 CALC_DIVI
# 今天的库
$md5sum libcalc.so
dcd6676d32d73e311e13a2626cd5db5d libcalc.so
$readelf --syms libcalc.so | grep -E "giNum1|giNum2|CALC_MULT|CALC_DIVI"
39: 0000000000201034 4 OBJECT LOCAL DEFAULT 22 giNum2
51: 0000000000201038 4 OBJECT GLOBAL DEFAULT 22 giNum1
55: 0000000000000710 19 FUNC GLOBAL DEFAULT 11 CALC_MULT
59: 0000000000000723 19 FUNC GLOBAL DEFAULT 11 CALC_DIVI
从这个角度来看,是不是可以表明只要是同一套代码并且使用相同的编译环境,符号表就可以共用呢?待实际验证。
参考资料
-
ELF文件详解-CSDN博客
目标文件_那个产物是目标文件-CSDN博客
file 指令后的文件信息解释-CSDN博客
linux C获取二进制文件的build-id_buildid-CSDN博客 -
Linux系统下列出库文件中的符号指令(nm)_nm linux-CSDN博客
nm命令使用详解,让你加快学习速度-CSDN博客 -
readelf nm objdump 命令详解_readelf objdump nm-CSDN博客
nm readelf objdump objcopy 命令之间的关系_readelf objdump nm-CSDN博客
objdump命令和nm命令有什么区别_nm与objdump的区别-CSDN博客
linux下nm,objdump和ldd三大工具使用_nm工具-CSDN博客