若是想完全弄懂Android代碼保護的基本原理,《Unix環境高級編程》和《程序員的自我修養》是必讀書目。在此做讀書筆記git
程序源代碼到最終可執行文件的4個步驟:程序員
主要處理那些源代碼文件中以"#"開始的預編譯指令github
gcc -E hello.c -o hello.i
複製代碼
對預編譯生成的文件進行詞法分析,語法分析,語義分析,中間語言生成,目標代碼生成及優化生成彙編代碼文件編程
gcc -S hello.i -o hello.s
複製代碼
彙編器將彙編代碼轉換成可執行指令,輸出目標文件bash
as hello.s -o hello.o`或者`gcc -c hello.s -o hello.o`或者`gcc -c hello.c -o hello.o
複製代碼
ld -static crt1.o crti.o crtbeginT.o -start-gruoup -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o
複製代碼
這裏省略了各文件的路徑函數
連接過程主要有以下步驟:工具
在Linux下可以使用file命令顯示文件格式優化
SimpleSection.c
代碼以下:ui
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i){
printf("%d\n", i );
}
int main(void){
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
複製代碼
查看目標文件內部的結構可使用objdump工具,可看到對應的各個段大體結構(-h),各個段詳細內容(-x),代碼段內容(翻譯成了彙編語言)(-s -d)spa
將某個二進制文件做爲目標文件的一個段
objcopy -I binary -o elf32-i386 -B i386 image.jpg image.o
將某個變量放在特定段
__attribute__((section("FOO"))) int global = 42
__attribute__((section("BAR"))) void foo()
類似段合併
空間與地址分配
掃描全部輸入目標文件,並得到他們各個段的長度、屬性和位置,並將輸入目標文件中的符號表全部的符號定義和符號引用收集起來,放到全局符號表中。因而,連接器將獲取全部輸入目標文件的段長度,並將他們合併,計算輸出文件中各個段合併後的長度和位置,創建映射關係。
符號解析與重定位
利用上一步的信息進行段的數據,讀取段數據,重定位信息,進行符號解析與重定位、調整代碼中的地址等。
以下圖:
編寫程序:
/*a.c*/
extern int shared;
int main(){
int a = 100;
swap(&a, &shared);
return 0;
}
/* b.c */
int shared = 1;
void swap(int *a, int *b){
*a ^= *b ^= *a ^=*b;
}
複製代碼
objdump -h a.o
objdump -h b.o
連接兩個文件ld a.o b.o -e main -o ab -lc
在個人機器上面須要加上-lc參數才能連接成功,否則報a.c:(.text+0x4b): undefined reference to `__stack_chk_fail錯誤,具體緣由不明,
-c: 從指定的命令文件讀取命令
-l: 把指定的存檔文件添加到要鏈接的文件清單
獲得的可執行文件不僅是簡單的連接過程,跟書中的內容有差別,有大神知道麻煩賜教
objdump -h ab
能夠看出,合併後獲得的ab文件的.text
段和.data
段的長度分別是9c和4,正好等於兩個.o文件相應段的長度之和。
重定位是靜態連接的核心內容,首先看a.o
裏面是如何訪問調用外部符號(shared
變量和swap
函數)
其中main
起始地址爲0x0,共佔用0x50個字節,最左邊那列表明偏移量偏移量爲22和31的地方即是分別引用sharead
變量和swap
函數的位置。
a.o
中引用shared
代碼爲lea 0x0(%rip),%rsi
,是將rip寄存器的值+0
直接傳遞給rsi
寄存器,這是由於還沒法查找符號shared
的位置,使用0x0代替,後面連接完成以後,ab文件就將0x0
替換爲0x200d47
,加上rsi
寄存器的值,計算後也就是shared
的地址0x0601020
,可以使用objdump -s ab
在data段
內看到該變量的值。
引用swap
函數的代碼爲callq 36 <main+0x36>
,既下一條指令的地址,ab文件則會直接將swap
地址0x400301
填入,變成callq 400301 <swap>
。
可是第二次試驗,是在公司電腦,我獲得的是以下結果
也就是說,沒有相對尋址了,這讓我有點納悶,swap
函數地址也是,不一樣於家裏電腦生成的指令。
總之,就是當文件並無連接以前,遇到了不認得的符號時,編譯器把地址0x0和下一條指令的地址做爲代替,等連接完成地址和空間的分配後,就已經能夠肯定全部符號的虛擬地址了,此時連接器再將全部須要重定位的指令進行地址修復。
書中的環境及解釋是這樣子的:
絕對尋址修正
a.o第一個重定位入口,即偏移爲18的mov指令修正,修正方式是R_386_32,即絕對地址修正。這個重定位入口,修正後應該是S+A
因此重定位入口修正後地址爲:0x3000+0x00000000=0x3000,指令修正後應該是:
相對尋址修正
a.o的第二個重定位入口,即偏移爲0x26這條call指令的修正,修正方式爲R_386_PC32,也就是相對地址修正。這個重定位入口,修正後結果應爲S+A-P
e8 fc ff ff ff
中操做數0xfc ff ff ff
(小端:-4)最後重定位入口修正後地址爲0x2000 + (-4) - (0x1000 + 0x27) = 0xFD5,即:
重定位表專門用於保存與重定位相關的信息,它在ELF文件中每每是一個或者多個段。對於可重定位ELF文件來講,一個重定位表每每就是ELF文件中的一個段,因此重定位表也能夠叫作重定位段。好比,代碼段「.text」若是有要被重定位的地方,那麼就會有一個相對應的「.rel.text」的段保存代碼段的重定位表,可以使用objdump來查看目標文件的重定位表:
objdump -r a.o
複製代碼
RELOCATION RECORDS FOR [.text]
表示這個重定位表是代碼段的重定位表重定位表的結構是一個Elf64_Rel or Elf32_Rel
結構,以下
struct Elf32_Rel
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
};
struct Elf64_Rel
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
};
複製代碼
連接是由於咱們的目標文件中用到的符號被定義在其餘目標文件當中,若是咱們直接使用ld來連接「a.o」,而不將「b.o」做爲輸入,則會出現shared
和swap
兩個符號未定義的狀況:
在開發過程當中,發生這種狀況的緣由有不少,最多見的狀況通常都是連接時缺乏某個庫文件或者輸入目標文件路徑不正確或者符號的聲明和定義不同。所以,從普通程序員的角度看,符號的解析佔據了連接過程的主要內容。
其實,重定位過程也伴隨着符號的解析過程,每個目標文件均可能定義一些符號,也可能引用到定義在其餘目標文件的符號。重定位過程當中,每一個重定位的入口都是對一個符號的引用,當鏈接器須要對某個符號的引用進行重定位時,就要肯定這個符號的目標地址。此時,連接器就會去查找全部輸入目標文件的符號表組成的全局符號表,找到對應的符號後進行重定位。
好比查看「a.o」的符號表
其中UND表示undefined
未定義類型。這種未定義的符號是由於該目標文件中有關於他們的重定位項。因此連接器掃描完全部輸入目標文件以後,這些未定義的符號都應該能夠在全局符號表中找到,不然就會報符號未定義錯誤。
不一樣的處理器指令對與地址的格式和方式都不同。但總的來講尋址方式有以下幾個方面:
近址尋址或遠址尋址
絕對尋址或相對尋址
尋址長度爲8位、16位、32位或64位
可是對於32位x86平臺下ELF文件重定位入口所修正的指令尋址方式只有兩種:
絕對近址32位尋址
相對近址32位尋址
書中的尋址方式是這個:
可是在公司,我機器是64位的,尋址方式是這個:
也就是R_X86_64_32
和R_X86_64_PC32
,網上也沒找到對應的資料,哪位大佬若是知道懇請指導,或者在git上面提issue。
不過我研究了一下,R_X86_64_32
的尋址方式,彷佛是直接將地址直接寫入修復便可:
家裏機器確定會不同的結果,所以這裏留一個坑待填。
在C語言中,函數和初始化的全局變量(包括顯示初始化爲0)是強符號,未初始化的全局變量是弱符號。咱們也能夠經過GCC的"__attribute__((weak))
"來定義任何一個強符號爲弱符號。
對於它們,下列三條規則使用:
① 同名的強符號只能有一個,不然編譯器報"重複定義"錯誤。
② 容許一個強符號和多個弱符號,但定義會選擇強符號的。
③ 當有多個弱符號相同時,連接器選擇佔用內存空間最大的那個。
若是一個弱符號定義在多個目標文件中,它們的類型又不一樣,而連接器自己有不支持符號類型,即變量類型對於連接器來講是透明的,此時若是類型不一致應該如何處理呢?主要分如下集中狀況:
第一種狀況是無需額外處理的,多強符號定義自己便是非法,連接器將報多重定義錯誤,連接器要處理後兩種狀況。
此時,COMMOM塊
機制出現。以SimpleSection.c
爲例子,符號表以下:
這裏能夠看到符號global_uninit_var
爲GLOBAL
數據對象,大小爲4,類型爲COM
,而實際上該變量爲弱類型 int
類型變量
另外編寫一個Common.c
文件內容以下:
double global_uninit_var = 24;
複製代碼
gcc -c Common.c SimpleSection.c
gcc -o Common Common.o SimpleSection.o
readelf -s Common
獲得以下結果:
能夠看到,size變成了8
但若是將SimpleSample.c
裏面的global_uninit_var
改成double
類型,把Common.c
裏面的global_uninit_var
改成int
類型,再執行可得以下警告:
這是由於弱符號大小大於強符號大小所致,此時結果以下,大小是4:
若是Common.c
裏面的global_uninit_var
也改成弱符號,則獲得的符號大小爲8
最後.bbs段大小爲8,即最終爲初始化全局變量仍是放在了bbs段。
這個時候咱們能夠得出以下結論:
咱們能夠想到,當編譯器將一個編譯單元編譯成目標文件的時候,若是該編譯單元包含了弱符號(未初始化的全局變量就是典型的弱符號),那麼該弱符號最終所佔空間的大小此時是未知的,由於有可能其餘編譯單元中同符號名稱的弱符號所佔的空間比本編譯單元該符號所佔的空間要大。因此編譯器此時沒法爲該弱符號在BSS段分配空間,由於所須要的空間大小此時是未知的。可是連接器在連接過程當中能夠肯定弱符號的大小,由於當連接器讀取全部輸入目標文件後,任何一個弱符號的最終大小均可以肯定了,因此它能夠在最終的輸出文件的BSS段爲其分配空間。因此整體來看,未初始化的全局變量仍是被放在BSS段。
GCC的
-fno-common
選項容許咱們把全部爲初始化的全局變量不以COMMON塊的形式處理,或者使用__attribute__
擴展:int global ____attribute__((nocommon));
主要有兩個:
C++編譯器會產生重複代碼,如模板(Templates),外部內聯函數(ExternInline Function)和虛函數表(Virtual Function Table)均可能在不一樣的編譯單元中生成相同代碼。
有效作法是將每一個模板實例單獨放在一個段裏面,每一個段包含一個模板實例。好比add<T>()
,某個編譯單元以int類型和float類型實例化該模板函數,那麼目標文件就包含了該模板實例的段,如.tmp.add<int>
和.tmp.add<float>
,當其餘編譯單元也須要相同的方式實例化該模板函數後,也會使用相同的名字,這樣在連接器最終連接的時候能夠區分這些相同的模板實例段,而後將它們併入最後的代碼段。
GCC把相似最終連接時合併的段叫作Link Once
,將這種類型的段命名爲.gnu.linkonce.name
,其中name
是該模板函數實例的修飾後名稱。
而VISUAL C++則將該類型的段叫作「COMDAT」,連接器會根據這個標記,在鏈接時將重複的段丟棄。
可是,當相同名稱的段可能有不一樣的內容,這多是不一樣編譯單元使用的編譯器版本或編譯優化選項不一樣。這時連接器頗有可能隨意選擇其中一個副本做爲連接的輸入,而後提供一個警告信息,一般狀況下,這種信息是不能隨意忽略的。
這是VISUAL C++提供的編譯選項「函數級別連接」,可將函數象上述方式那樣把函數放在單獨的段中,能夠作到沒有用到的函數則將它拋棄。
GCC 也提供相似的機制
-ffunction-sections
:將函數分別保持到獨立的段中-fdata-sections
:將變量分別保持到獨立的段中Linux系統下通常程序入口爲_start
,這個函數是Linux系統庫(Glibc)的一部分。當程序和Glibc庫連接到一塊兒造成最終可執行文件之後,這個函數就是程序的初始化部分入口。
ELF文件定義以下兩個特殊段
.init
該段保存可執行指令,構成進程的初始化代碼,main函數被調用前,Glibc的初始化部分安排執行這個段中的代碼。
.fini
該段保存着進程終止代碼指令。當main函數正常退出時,Glibc會安排執行這個段中的代碼。
利用這個特性,C++全局構造和析構函數便由此實現。
靜態連接庫實際上能夠當作是一組目標文件的集合。使用ar壓縮程序可將這些目標文件壓縮在一塊兒,並對其進行編號和索引,以便於查找和檢索。
ar -t libc.a
查看文件包含哪些目標文件
objdump -t libc.a grep xxx
查找某個函數所在目標文件
ar -x libc.a
解壓出目標文件
連接過程能夠十分複雜,以printf函數爲例: