bfd介绍
想深入了解elf等可执行文件的原理(包括结构、运行等细节),用bfd库作切入点是比较好的选择。
BFD是Binary format descriptor的缩写, 即二进制文件格式描述,是很多可执行文件相关二进制工具(如nm、objdump、ar、as等命令)的基础库。bfd库可以用来分析、创建、修改二进制文件,支持多种平台(如x86、arm等)及多种二进制格式(如elf、core、so等)。
bfd库编译
bfd库是gnu开源软件的一部分,可以在gnu的binutils(即binary utils二进制工具集)目录中找到,地址是http://www.gnu.org/software/binutils/,源码路径为http://ftp.gnu.org/gnu/binutils/,源码分析一般基于较低的版本进行分析,低版本一般来说代码量少一些,特性少一些,但核心的东西都会具备,就像内核代码,0.11版本还是学习内核源码的经典。本来想直接分析binutils-2.7.tar.gz的,但2.7的源码不支持i686-linux-gnu平台(96年的20多年了),后选用了binutils-2.13.2.1.tar.gz版本进行分析,后续都是基于2.13版本分析。
binutils-2.13.2.1.tar.gz这个软件包,里面包含了bfd库、nm等命令,编译通过后,bfd库与nm都编译出来了,编译很简单,编译步骤:
1. 代码解压到linux目录中
sw@sw:mypro$ tar xzvf binutils-2.13.2.1.tar.gz
2. 执行configure命令
sw@sw:binutils-2.13.2.1$ ./configure
3. 直接make编译
我的机器上编译过程会报错,gcc版本是gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3),报错如下:
./config/tc-i386.h:530:32: error: array type has incomplete element type
extern const struct relax_type md_relax_table[];
./config/tc-i386.h:531:32: error: ‘md_relax_table’ undeclared (first use in this function)
#define TC_GENERIC_RELAX_TABLE md_relax_table
把gas/config/tc-i386.h的530及531行直接删除即可编译通过。
//extern const struct relax_type md_relax_table[];
//#define TC_GENERIC_RELAX_TABLE md_relax_table
编译出的bfd库在bfd/.libs即libbfd.a,nm在binutils目录下,名称为nm-new。
nm源码分析
学习bfd库,可以通过nm命令入手,gdb也用到了bfd库,使用gdb的源码阅读bfd库也是可行的,但gdb源码较复杂,而nm命令实现很简单,使用了较少的bfd接口,看过nm源码,就对bfd库的使用有大致的了解,为阅读bfd源码作准备,通过自己编写nm命令,很方便阅读与调试(打印调试或gdb调试)bfd库。
先看一下main函数,选项部分先不管,先按nm不带参数进行分析
int main (argc, argv)int argc; char **argv;
{
bfd_init (); /* 初始化bfd库 */
set_default_bfd_target ();/* 设置默认的目标平台 */
if (!display_file (argv[optind++])) /* */
...
}
主要功能函数是display_file函数,核心代码如下:
static boolean display_file (filename) char *filename;
{
file = bfd_openr (filename, target); /* 打开bfd库 */
//...
if (bfd_check_format (file, bfd_archive))
{
display_archive (file);
}
/* 检查分析的文件是否为 bfd_object类型,elf属这种类型 */
else if (bfd_check_format_matches (file, bfd_object, &matching))
{
char buf[30];
bfd_sprintf_vma (file, buf, (bfd_vma) -1);
print_width = strlen (buf);
(*format->print_object_filename) (filename);
display_rel_file (file, NULL); /* 显示相关文件符号 */
}
//...
if (bfd_close (file) == false) /* 关闭bfd库 */
//...
}
nm使用的对象暂先定位elf可执行文件,上面代码用的是 bfd_object这个分支。bfd_sprintf_vma先不用管,看bfd代码再说,(*format->print_object_filename) (filename);这一句注意一下,代码如下:
static struct output_fns *format = &formats[FORMAT_DEFAULT];
static struct output_fns formats[] =
{
{
print_object_filename_bsd,
print_archive_filename_bsd,
print_archive_member_bsd,
print_symbol_filename_bsd,
print_symbol_info_bsd
},
}
这个结构体数组加函数指针,最终 (*format->print_object_filename) 指向的函数是 print_object_filename_bsd,这个函数没什么内容,把这个函数提出来说一下的原因是,像bfd这类兼容多平台、多文件类型的代码,结构体数组加函数指针的用法非常普遍。再看一下 display_rel_file 函数代码:
static void display_rel_file (abfd, archive_bfd) bfd *abfd; bfd *archive_bfd;
{
/* 读取bfd最小符号表 */
symcount = bfd_read_minisymbols (abfd, dynamic, &minisyms, &size);
/* 过滤不需显示的符号 */
symcount = filter_symbols (abfd, dynamic, minisyms, symcount, size);
//...
if (! no_sort)
{
if (! sort_by_size) /* 对符号按名称排序 */
qsort (minisyms, symcount, size,sorters[sort_numerically][reverse_sort]);
}
if (! sort_by_size) /* 打印符号表 */
print_symbols (abfd, dynamic, minisyms, symcount, size, archive_bfd);
free (minisyms); /* */
}
符号的过滤与排序可以先不管,主要关注 print_symbols 函数
static void print_symbols (abfd, dynamic, minisyms, symcount, size, archive_bfd)
{
for (; from < fromend; from += size)
{
/* 把minisymbol结构转成symbol,方便打印符号 */
sym = bfd_minisymbol_to_symbol (abfd, dynamic, from, store);
/* 打印符号 */
print_symbol (abfd, sym, (bfd_vma) 0, archive_bfd);
}
}
print_symbol 函数最主要执行 (*format->print_symbol_info) (&info, abfd);
语句,最终执行的是 print_symbol_info_bsd 函数。
以上是nm命令简单的流程,基本就是调bfd接口实现。
基于bfd库的简单nm命令实现
nm命令调用的bfd函数较少,为方便理解bfd使用流程,可以把里面的函数抽出来,写一个简单的nm命令,以此为基础,不断增加命令,加深对bfd函数用法的理解。下面是最简单的nm实现源码,大家以此为基础增加功能,进一步理解bfd库。代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <linux/elf.h>
#include "bfd.h"
#define TARGET "i686-pc-linux-gnu"
int main(int argc, char * argv[])
{
int ret = 0, i = 0, size = 0, dynamic = 0;
long symcount = 0;
bfd * ibfd = NULL;
char * from = NULL;
PTR minisyms;
asymbol * sym = NULL, *store = NULL;
symbol_info syminfo;
bfd_init();
#if 0
bfd_set_default_target(TARGET);
ibfd = bfd_openr(argv[1], TARGET);
#else
ibfd = bfd_openr(argv[1], NULL);
#endif
if (ibfd == NULL)
{
return -1;
}
ret = bfd_check_format_matches(ibfd, bfd_object, NULL);
if (ret <= 0)
{
return -1;
}
symcount = bfd_read_minisymbols(ibfd, dynamic, &minisyms, &size);
if (symcount <= 0)
{
return -1;
}
for (i = 0; i < symcount; i++)
{
store = bfd_make_empty_symbol(ibfd);
if (store == NULL)
{
continue;
}
from = (char *)minisyms + i * size;
sym = bfd_minisymbol_to_symbol(ibfd, dynamic, from, store);
if (sym)
{
bfd_get_symbol_info(ibfd, sym, &syminfo);
printf("%#010lx %c %-s\n",syminfo.value,syminfo.type,syminfo.name);
}
}
free(minisyms);
bfd_close(ibfd);
return 0;
}
编译命令依赖bfd库和liberty库,这两个库编译binutils会生成,注意改路径,如下
gcc test.c -g -o test -lbfd -liberty -I../binutils-2.13.2.1/bfd -L../binutils-
2.13.2.1/bfd/.libs -L../binutils-2.13.2.1/libiberty/
这个nm命令不支持选项、没过滤以及排序功能。
bfd接口说明
bfd可以用的接口非常多,用法主要参考官方文档:https://sourceware.org/binutils/docs-2.30/bfd/index.html ,文档包括前段与后端,我们主要关注前端文档,即第二节的内容,不太明白的地方自己再验证,下面介绍一些上面用到的接口。
bfd初始化
void bfd_init (void);
bfd初始化,反正2.13版本这个函数是空的,但官方文档上要求初始化时调用该函数。
bfd target选择
bfd_boolean bfd_set_default_target (const char *name);
const bfd_target *bfd_find_target (const char *target_name, bfd *abfd);
目标平台一般情况下不要设置,bfd库会自动确定。
bfd打开及关闭文件
bfd *bfd_openr (const char *filename, const char *target);
bfd *bfd_fdopenr (const char *filename, const char *target, int fd);
bfd_boolean bfd_close (bfd *abfd);
bfd打开文件函数有很多个,用在不同场合,打开时,target可以填NULL,填NULL后,bfd库会自动查找target,要填的话,一定要填对。打开成功后返回bfd结构体,后续操作都是基于该结构体的。操作完成后都需要调用关闭文件接口以释放资源。
bfd获取minisymbols
long bfd_read_minisymbols(bfd , boolean, PTR ,unsigned int *);
asymbol bfd_minisymbol_to_symbol(bfd , boolean, const PTR,asymbol *);
minisymbols接口以只读方式读取符号表,该类接口使用较少的内存,但这类接口更耗时,可以处理符号表非常大的情况,常用在nm、objdump等工具。
该bfd_read_minisymbols函数将以内部形式将符号读入内存,返回符号的个数,minisyms指针是使用malloc分配内存,用完后需注意释放,bfd_minisymbol_to_symbol函数把指针转为asymbol结构,需先申请一个空的symbol。