任何技藝的提升都在於積累與總結;而對工具與流程的認識,有利於我們更深入的理解系統結構與執行環境。
任何東西都要硬幣一樣有正反兩面:
感謝GNU,爲我們提供了一整套完整的開發工具與運行環境,讓我們能夠更容易地開發與理解軟件系統。
唾棄GNU,爲我們提供了一整套完整的開發工具與執行環境,讓我們在複雜的軟件環境中沉重的學習,同時也喪失了自我的“創造力”。
然而對於工具與環境的把控,也正是提升與提煉自己的好機會,能讓我們更加深入的理解軟件系統,從而才能創造出更完備,更精煉的程序與系統。
認識ELF文件與Binutils工具,是我們熟悉編譯工具的基本步驟;因爲在Linux平臺或者說GNU環境下的編譯器——GCC,只是工具中重要的一環與很友善的接口界面而已。
一.ELF文件(在linux上的可執行與鏈接的二進制文件)的要點:
1.在linux平臺中存在的主要形式:*.o(編譯與彙編後的目標文件),*.so(鏈接之後的動態鏈接庫),*.out(鏈接之後的可執行文件),*.ko(編譯鏈接之後的驅動模塊);
2.分析二進制文件的一個很有效的方式,就是用對應的頭文件進行數據模塊化的讀取,而ELF文件的完整描述方式爲elf.h文件:http://linux.die.net/include/elf.h;
3.ELF文件簡介,該文件主要由ELF頭信息與程序頭信息表或者段表組成,詳細的描述見:http://linux.die.net/man/5/elf;或者http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic.html#ELF-GENERIC
二.Binutils工具(該工具一般不需要編譯,只有在製作交叉編譯工具時)介紹http://www.sourceware.org/binutils/:
1.主要內容,參考LFS7.6(Linux From Scratch)6.13.2:
a)工具集——默認的命令行有很多參數,但是實際使用的確很少,用如下表簡單總結一下
命令 |
功能 |
常用參數 |
意義 |
實例 |
說明 |
ar |
創建、修改、抽取靜態庫(*.a歸檔文件) |
r |
創建 |
ar -r libXX.a YY.o |
|
d |
刪除obj |
ar -d libXX.a YY.o |
|
||
x |
抽取obj |
ar -x libXX.a YY.o |
|
||
t |
查看有哪些成員 |
ar -t libXX.a |
|
||
nm |
列舉obj文件的符號表 |
|
|
nm XXX |
|
objcopy |
拷貝與轉換格式obj文件中的部分內容 |
j |
提取elf文件分區 |
objcopy -j XX YY |
|
R |
刪除elf文件分區 |
objcopy -R XX YY |
|
||
S |
刪除符號表 |
objcopy -S XX YY |
|
||
I |
輸入文件bfd格式 |
objcopy -I binary -O bfd -B arch XX YY |
將二進制文件編入程序的方法,如何得到bfd格式(objdump -i) |
||
O |
輸出文件bfd格式 |
||||
B |
輸出目標函數構架 |
||||
|
|
objcopy -O binary -j sec objfile obj-sec.bin |
抽取obj文件的某部分爲bin文件 |
||
objdump |
讀取odj文件中具體信息 |
i |
讀取所支持的bfd格式 |
objdump -i |
|
d/D |
反彙編 |
objdump -d -j .text XXX |
反彙編且將源碼嵌入到彙編中:objdump -S -l -D/-z -j obj |
||
t |
符號表 |
objdump -t XXX |
|
||
S |
添加源碼 |
objdump -d -j .text XXX -S |
|
||
m |
指定執行的平臺 |
objdump -d -j .text XXX -S -m YYY |
通過它可以將Intel彙編與AT&T彙編互換 |
||
j |
某個段 |
objdump -d -j .text XXX |
|
||
|
|
|
|
|
|
ranlib |
爲靜態庫生成符號索引表 |
|
|
ranlib libXX.a |
nm -s 查看,等價於ar -s |
|
|
|
|
|
|
size |
讀取文件的代碼段、數據段大小 |
|
|
size objfile |
|
|
|
|
|
|
|
strings |
列出elf文件中所有的字符串 |
|
|
strings elffile |
|
|
|
|
|
|
|
strip |
清除obj文件的符號信息 |
s |
所有的字符串 |
strip objfile |
|
|
|
|
|
|
|
c++filt |
通過編譯之後的符號名得到它實際被聲明的函數 |
j |
也支持java的語法 |
c++filt symbol |
|
|
|
|
|
|
|
addr2line |
得到帶debug信息的elf文件的某個地址,所在源文件的行 |
e |
輸入源文件 |
addr2line -e elf addr |
一定要gcc -g |
|
|
|
|
|
|
readelf |
分析elf文件的基本結構 |
h |
讀取elf頭 |
readelf -h XX |
|
l |
讀取程序頭 |
readelf -l XX |
|
||
S |
讀取分區表 |
readelf -S XX |
|
||
d |
讀取動態表 |
readelf -d XX |
ldd 查看elf文件所依賴的動態庫,並且可以得到加載地址 |
||
s |
讀取符號表 |
readelf -s XX |
|
更具體的命令分析,見附件。
b)相關函數庫:
libbfd.a用於描述與操作可執行二進制文件(BFD)。
libopcodes.a用於實現彙編與反彙編的操作。
libiberty.a可以看作一個很有用的庫,用於內存管理(obstack),命令行參數的統一實現等。
c)默認鏈接腳本,安裝與ldscript的目錄下。鏈接腳本的主要目的是被鏈接器(ld)將輸入的BFD文件根據腳本規則,映射成輸出的文件。如下整理的圖,描述其語法:
三.Binutils工具在一般人看來是沒有多少用,而且非常複雜,難以理解;但是對於想去理解與分析系統的人,確是必備的基礎;以下就是這些命令常用的一些場合總結用於進一步理解與熟練使用它們
1.objdump的反彙編有助於查看c語言源代碼:
編譯程序時,用-g參數,添加相關的debug信息到elf文件中。
objdump -j.text -d -S elf-file
爲什麼要用反彙編的方式呢?因爲這樣可以將各種宏定義給去掉,方便直接查看源代碼。
2.objcopy將普通文件作爲資源文件編譯到程序中被使用,比如:
a)編譯hello.txt文件:hello world。
b)將hello.txt轉換爲bfd文件(hello.o),用於gcc鏈接,以x86_64的處理器構架爲例(查詢構架名與bfd名的方法爲:objcopy --info):
objcopy -I binary -O elf64-x86-64 -B i386 hello.txt hello.o
c)在程序中引用該文件生成的段。
extern char _binary_hello_txt_start[];//數據開始
extern char _binary_hello_txt_end[];//數據結束
int main()
{
int i;
printf("start : 0x%x\n",_binary_hello_txt_start);
printf("end : 0x%x\n",_binary_hello_txt_end);
for(i=0;i<_binary_hello_txt_end-_binary_hello_txt_start;i++)
printf("%c",*((char*)_binary_hello_txt_start+i));
printf("\n");
}
3.objdump反彙編某個函數(objdump_func.sh elf-file fun_name):
target_elf=$1
func_name=$2
if [ -z "$target_elf" ];then
echo "input obj-elf file is empty";
exit;
fi
if [ -z "$func_name" ];then
objdump -j.text -d $target_elf
exit;
Fi
#通過readelf 讀取函數的基本信息開始地址與佔用的大小
func_info=`readelf -s $target_elf|egrep "\b$func_name\b"`;
start_addr=`echo $func_info|awk '{print $2}'`
size=`echo $func_info|awk '{print $3}'`
stop_addr=$((16#$start_addr+16#$size))
echo $func_name at $target_elf: 0x$start_addr -- $stop_addr
#通過計算的start_addr(開始地址)與stop_addr(結束地址)進行直接
#反彙編
objdump -j.text -d $target_elf --start-address=0x$start_addr --stop-address=$stop_addr
4.用strip工具爲編譯生成的elf文件進行“瘦身”操作,去掉符號表;在嵌入式系統中,很有意義,減少生產的bin檔佔用的空間。
strip elf-file
5.用addr2line去分析程序死掉的地方(程序是由gcc -g生成的,帶debug信息)。
程序運行在虛擬地址空間,而對於某種構架的編譯器生成的程序地址一般都是固定的(默認的程序進入點);當linux的kernel崩潰時或者應用程序崩潰時,會將堆棧信息與寄存器信息打印出來。其中就有很多地址信息,通過addr2line就可以將地址映射成源代碼中的某一行。
6.理解bootloader的腳本,在MIT的操作系統實踐項目(http://pdos.csail.mit.edu/6.828/2012/labs/lab1/)中,在MBR中引導操作系統的代碼編譯make中有如下信息:
$(OBJDIR)/boot/boot: $(BOOT_OBJS)
@echo + ld boot/boot
#用ld鏈接成的obj文件 進入點位 start標記,代碼段的開始點位0x7c00.
#因爲bios加載MBR之後,會在0x7c00上執行代碼
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o [email protected] $^
#用objdump將生產的elf文件,代碼反彙編爲$(OBJDIR)/boot/boot.asm, #而且帶源 碼 信息
$(V)$(OBJDUMP) -S [email protected] >[email protected]
#用objcopy 將 代碼段拷貝爲$(OBJDIR)/boot/boot,用於燒寫到MBR引導 #操作系統
$(V)$(OBJCOPY) -S -O binary -j .text [email protected] $@
#修改512byte的boot最後分區爲0x55AA用於指明該分區用於引導系統
$(V)perl boot/sign.pl $(OBJDIR)/boot/boot
7.uboot的編譯過程的腳本分析
a)鏈接腳本的使用(以arm處理器爲例:u-boot-2014.10\arch\arm\cpu):
#include <config.h>
/*代碼最終輸出格式*/
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*處理器構架*/
OUTPUT_ARCH(arm)
/*程序進入點*/
ENTRY(_start)
SECTIONS
{
/*代碼段,從0x0開始*/
. = 0x00000000;
. = ALIGN(4);
/*定義代碼段的鏈接內容*/
.text :
{
*(.__image_copy_start)
*(.vectors)
CPUDIR/start.o (.text*)
*(.text*)
}
...
}
b)Makefile中有各種格式轉換與鏈接到特定的地址
8.用ar命令打包obj文件,生成靜態鏈接庫
ar rcs lib$fn.a $fn.o
9.用c++filt得到c++與java在源代碼中的實際名稱。
a)寫一個測試demo:
#include <stdio.h>
class A{
private: int a;
private: int b;
public:
A(int a,int b){
this->a =a;
this->b = b;
}
int add()
{
return a+b;
}
};
int main()
{
A Test(3,5);
printf("%d\n",Test.add());
}
b)通過readelf得到關於A::add()的編譯生成最後的名稱爲:
57: 0000000000400656 25 FUNC WEAK DEFAULT 13 _ZN1A3addEv
58: 0000000000601038 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
59: 00000000004006f8 0 OBJECT GLOBAL HIDDEN 15 __dso_handle
60: 0000000000400670 101 FUNC GLOBAL DEFAULT 13 __libc_csu_init
61: 0000000000601034 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
62: 0000000000400632 35 FUNC WEAK DEFAULT 13 _ZN1AC1Eii
63: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 25 _end
64: 0000000000400632 35 FUNC WEAK DEFAULT 13 _ZN1AC2Eii
c)通過c++flit得到正常的名稱:
c++filt _ZN1A3addEv --->A::add()
c++filt _ZN1AC1Eii ------>A::A(int, int)