GNU Binutils

一、ELF 文件

在介绍 GNU Binutils 前,先来对 ELF 文件做个简单的了解,因为后续所有操作都将围绕 ELF 文件展开。

1.1 ELF的定义

ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的 ELF 格式文件包括:

  1. 可执行文件(.exe)
  2. 可重定位文件(.o)
  3. 共享目标文件(.so)
  4. 核心转储文件(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
    giNum2
    STT_FUNC 2 该符号是个函数或其他可执行代码 CALC_*
    STT_SECTION 3 该符号表示一个段,这种符号必须是 STB_LOCAL 的 -
    STT_FILE 4 该符号表示文件名,一般都是该目标文件所对应的原文件名
    它一定是 STB_LOCAL 类型的,并且它的 st_shndx 一定是 SHN_ABS
    calc.c
  • Bind:绑定信息

    名称 说明 举例
    LOCAL 局部符号,对目标文件外部不可见 giNum2
    CALC_SQRT
    GLOBAL 全局符号,外部可见 giNum1
    CALC_MULT
    CALC_DIVI
    WEAK 弱引用 -
  • Ndx:表示为符号所在的段

    名称 说明 举例
    ABS 表示该符号包含一个绝对值 -
    COMMON 一般表示为初始化的全局符号 -
    UNDEF 表示该符号未定义
    在本目标文件中被引用但是定义在其他目标文件中
    sqrt

2.4 objdump

objdump 是一个 GCC 工具:

  1. 可以查看目标文件或者可执行文件的各个段信息。
  2. 可以显示有关文件的大量不同信息。
  3. 也可以显示符号,并且在联机帮助页中明确指出「这与 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 命令的输出包含三个部分:

  1. 符号偏移量,默认显示十六进制,也可以指定

  2. 符号类型,小写表示是本地符号,大写表示全局符号(external)

    符号类型 含义
    b / B 符号位于未初始化数据段
    用 0 初始化或未初始化
    d / D 符号已初始化
    t / T 代码段的符号
    U 未定以的符号
  3. 符号名称

选项 简写 说明 备注
--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 的符号导出操作了:

  1. 对外发布使用 strip 后的 libcalc.so
  2. 问题定位使用 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 输出字段解释

  1. ELF 64-bit LSB shared object
    • ELF 表示可执行和可链接格式(Executable and Linkable Format),是一种用于可执行文件、目标文件和共享库的标准文件格式;
    • 64-bit LSB 表示该可执行文件是 64 位的,并且使用了「Least Significant Byte」顺序。
  2. version 1 (SYSV)
    • version 1 表示 ELF 文件的版本;
    • (SYSV) 表示它遵循 System V ABI(Application Binary Interface)标准。
  3. dynamically linked
    • 表明这个可执行文件使用了动态链接(dynamically linked)这意味着它依赖于系统上已安装的共享库,在运行时才会解析和加载这些库。
  4. BuildID[sha1]=
    • 提供了构建标识符,用于唯一标识二进制文件的构建,这对于调试和版本控制很有用。
  5. 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

从这个角度来看,是不是可以表明只要是同一套代码并且使用相同的编译环境,符号表就可以共用呢?待实际验证。

参考资料

posted @ 2024-04-20 23:11  MElephant  阅读(104)  评论(0编辑  收藏  举报