談?wù)劤绦騿?dòng)那點(diǎn)事

本文主要是《程序員的自我修養(yǎng)》學(xué)習(xí)筆記送爸,并著重闡述了程序啟動(dòng)的流程及Mac相關(guān)的知識(shí)點(diǎn)铛嘱;

一、簡介

程序內(nèi)存通過分段形式來直接建立虛擬地址與實(shí)際物理地址之間的映射袭厂,若內(nèi)存不足需要置換出段墨吓,導(dǎo)致效率低下;根據(jù)程序的局部性原理纹磺,可以將內(nèi)存分割為更小粒度的內(nèi)存帖烘,提高內(nèi)存使用率,即“分頁”橄杨;

“線程”共享進(jìn)程的內(nèi)存里的所有數(shù)據(jù)秘症,甚至包括其他線程的堆棧,但實(shí)際運(yùn)用中線程也擁有自己的私有存儲(chǔ)空間式矫,包括:

  • 棧(盡管并非完全無法被其他線程訪問乡摹,但一般情況下仍然可以認(rèn)為是私有的數(shù)據(jù))
  • 線程局部存儲(chǔ)(Thread Local Storage, TLS),線程局部存儲(chǔ)是某些操作系統(tǒng)為線程單獨(dú)提供的私有空間采转,但通常只具有很有限的容量
  • 寄存器

一般把頻繁等待的線程稱之為“I/O密集型線程”聪廉,而很少等待的線程稱之為“CPU密集型線程”;

“函數(shù)可重入”故慈,表示函數(shù)沒有執(zhí)行完成板熊,由于外部因素或內(nèi)部因素,由一次進(jìn)入該函數(shù)執(zhí)行察绷。一個(gè)函數(shù)要被沖入干签,只有兩種情況:

  • 多個(gè)線程同時(shí)執(zhí)行這個(gè)函數(shù)
  • 函數(shù)自身調(diào)用自身

編譯器過度優(yōu)化會(huì)導(dǎo)致不可預(yù)期的行為,如線程加鎖后仍然行為異常拆撼,如下:

x = 0;
Thread1         Thread2
lock();         lock();
x++;                x++;
unlock();       unlock();

如果編譯器為了提高變量x的訪問速度筒严,把x放到了某個(gè)寄存器里丹泉,因?yàn)椴煌€程的寄存器是各自獨(dú)立的(可以理解為線程控制塊存有私有的寄存器值情萤,當(dāng)線程切換時(shí)會(huì)保存私有的寄存器值鸭蛙,當(dāng)線程切換回來會(huì)恢復(fù)寄存器的值),可能會(huì)出現(xiàn)如下情況:

  • [Thread1] 讀取x的值到寄存器R1
  • [Thread1] R1++(由于之后可能還要訪問x筋岛,因此暫不會(huì)將R1寄存器的值寫回內(nèi)存變量x)
  • [Thread2] 讀取x的值到某個(gè)寄存器R2
  • [Thread2] R2++并寫回到內(nèi)存中
  • [Thread1] 將寄存器R1的值寫回內(nèi)存中

上述情況會(huì)導(dǎo)致變量x的值不相同娶视。

為防止編譯器過度優(yōu)化,可使用volatile關(guān)鍵字睁宰,其行為如下:

  • 阻止編譯器為了提高速度將一個(gè)變量緩存到寄存器而不寫回
  • 阻止編譯器調(diào)整操作volatile變量的指令順序肪获;

Linux中使用clone(帶有CLONE_VM參數(shù))產(chǎn)生的用戶態(tài)線程與內(nèi)核態(tài)線程是一一對(duì)應(yīng)得

二柒傻、靜態(tài)鏈接

現(xiàn)在的繼承開發(fā)環(huán)境(IDE)孝赫,一般都將編譯和鏈接的過程一步完成,稱為“構(gòu)建”红符;

程序構(gòu)建過程:預(yù)處理(Prepressing)->編譯(Compilation)->匯編(Assembly)->鏈接(Linking)青柄;


gcc編譯過程分解

預(yù)編譯

具體gcc預(yù)編譯選項(xiàng)為-E

gcc -E hello.c -o hello.i

預(yù)編譯文件的后綴為.i,主要處理規(guī)則如下:

  • 將所有#define刪除并展開所有的宏定義预侯;

  • 處理所有條件預(yù)編譯指令致开,如#if#ifdef萎馅、#elif双戳、#else#endif等糜芳;

  • 處理#include預(yù)編譯指令楣铁,將被包含的文件插入到該預(yù)編譯指令的位置;

  • 刪除所有的注釋//蝗柔、/* */

  • 添加行號(hào)和文件名標(biāo)識(shí)盅惜,用于編譯器產(chǎn)生調(diào)試用的行號(hào)信息以及產(chǎn)生編譯錯(cuò)誤或警告時(shí)能夠顯示行號(hào);

  • 保留所有的#pragma編譯器指令邪驮;

在所有的預(yù)處理指令中莫辨,#pragma 指令可能是最復(fù)雜的了,它的作用是設(shè)定編譯器的狀態(tài)或者是指示編譯器完成一些特定的動(dòng)作毅访。如#pragma once是一個(gè)非標(biāo)準(zhǔn)但是被廣泛支持的前置處理符號(hào)沮榜,會(huì)讓所在的文件在一個(gè)單獨(dú)的編譯中只被包含一次。以此方式喻粹,#pragma once提供類似include防范的目的蟆融,但是擁有較少的代碼且能避免名稱的碰撞,如下頭文件hello.h

#pragma once
struct data_t {
  xxx
}

編譯

編譯過程是把預(yù)處理完的文件進(jìn)行一系列詞法分析守呜、語法分析型酥、語義分析及優(yōu)化生成相應(yīng)的匯編代碼文件山憨,gcc選項(xiàng)為-S,命令如下:

gcc -S hello.i -o hello.s

具體編譯的結(jié)果如下:


編譯

匯編

匯編器是將匯編代碼轉(zhuǎn)換成機(jī)器可以執(zhí)行的指令弥喉,匯編就是匯編語句翻譯成對(duì)應(yīng)的機(jī)器指令郁竟,因此不會(huì)涉及語義及指令優(yōu)化,具體gcc命令如下:

gcc -c hello.s -o hello.o

經(jīng)過預(yù)編譯由境、編譯棚亩、匯編后直接輸出目標(biāo)文件(Object File);


匯編

鏈接

使用ld命令工具將不同的目標(biāo)文件鏈接稱為可執(zhí)行文件虏杰,后續(xù)將重點(diǎn)闡述讥蟆。

編譯器

編譯過程一般可以分為6步:掃描、語法分析纺阔、語義分析瘸彤、源代碼優(yōu)化、代碼生成和目標(biāo)代碼優(yōu)化笛钝。


編譯器
詞法分析

主要是將代碼中的關(guān)鍵字质况、標(biāo)識(shí)符、字面量(包括數(shù)字婆翔、字符串等)和特殊符號(hào)(如加號(hào)拯杠、等號(hào))。

lex工具可以實(shí)現(xiàn)詞法掃描

語法分析

語法分析就是對(duì)詞法分析產(chǎn)生的記號(hào)通過上下文無關(guān)語法的分析手段進(jìn)行分析啃奴,從而生成以表達(dá)式為節(jié)點(diǎn)的語法樹潭陪。

語法分析也有一個(gè)現(xiàn)成的工具叫做yacc

語義分析

語義分析,就是完成對(duì)表達(dá)式的語法層面的分析最蕾,分為“靜態(tài)語義”和”動(dòng)態(tài)語義“依溯;所謂的“靜態(tài)語義”是指在編譯期可以確定的語義,與之對(duì)應(yīng)的“動(dòng)態(tài)語義”就是只有在運(yùn)行期才能確定的語義瘟则。

”靜態(tài)語義“通常包括聲明和類型的匹配黎炉,類型的轉(zhuǎn)換〈着。“動(dòng)態(tài)語義”慷嗜,如運(yùn)行期才能確定的0作為除數(shù)是一個(gè)運(yùn)行期語義錯(cuò)誤。

優(yōu)化

主要包括源代碼優(yōu)化和目標(biāo)代碼優(yōu)化丹壕,如去除中間變量庆械,選擇合適的尋址方式,使用位移來代替乘法運(yùn)算菌赖,刪除多余的指令等缭乘,但生成的目標(biāo)代碼文件缺少未知變量的地址?因此需要通過鏈接來完成琉用。

鏈接

鏈接就是將多個(gè)目標(biāo)代碼文件之間相互引用的符號(hào)進(jìn)行重定位找到其地址堕绩,主要包括地址和空間分配策幼、符號(hào)決議(也稱符號(hào)綁定或者名稱綁定,甚至地址綁定奴紧、指令綁定)和重定位等步驟特姐。

目標(biāo)文件

格式

目標(biāo)文件就是源代碼編譯后但未進(jìn)行鏈接的中間文件(Linux下格式為.o),其可執(zhí)行文件的內(nèi)容與結(jié)構(gòu)很相似;

可執(zhí)行文件的格式:linux的格式為ELF(Executable Linkable Format)绰寞,靜態(tài)和動(dòng)態(tài)鏈接庫到逊、核心轉(zhuǎn)儲(chǔ)文件(core dump file)都是以該格式存儲(chǔ),可通過file命令查看文件格式滤钱。

目標(biāo)文件內(nèi)存包含了:機(jī)器指令代碼、數(shù)據(jù)脑题,還包括鏈接時(shí)所需要的一些信息件缸,如符號(hào)表、調(diào)試信息叔遂、字符串等他炊,以“段”(Segment)的形式存儲(chǔ)。

目標(biāo)文件包含代碼段已艰、數(shù)據(jù)段(包括已初始全局變量和局部靜態(tài)變量.data痊末;只讀數(shù)據(jù)段.rodata,一般是程序的只讀變量哩掺,如const修飾的變量和字符串常量)凿叠、bss段(未初始化的全局變量和局部靜態(tài)變量)、注釋段嚼吞、字符串表(.strtab或者.shstrtab盒件,分別保存普通字符串和段表字符串表)、符號(hào)表(.symtab舱禽,包括符號(hào)名+符號(hào)值)炒刁、調(diào)試段(.debug)等;

可通過binutils中的objdump工具來查看目標(biāo)文件內(nèi)部結(jié)構(gòu)誊稚,如-h顯示各個(gè)段的基本信息翔始,-d代碼段反匯編;或者使用readelf工具來輸出目標(biāo)文件的內(nèi)容里伯,對(duì)于Mac平臺(tái)為otool工具城瞎,通過size命令來查看ELF文件的代碼段、數(shù)據(jù)段和BSS段的長度俏脊;

分段的原因:

  • 不同段可被映射到不同虛擬存儲(chǔ)區(qū)域全谤,便于讀寫權(quán)限管理;
  • 利用現(xiàn)代CPU緩存體系及程序的局部性原理爷贫,將指令和數(shù)據(jù)緩存分離有利用提升緩存命中率认然;
  • 指令或數(shù)據(jù)共享补憾,有利于提升內(nèi)存空間利用率;
    image.png

    對(duì)如上代碼gcc -c -g hello.c -o hello.o生成目標(biāo)文件hello.o卷员,并使用objdump -s -d hello.o查看段內(nèi)容盈匾,其中常量數(shù)據(jù)段__const內(nèi)容如下:
    image.png

    上述__const段內(nèi)容正是const int c = 100對(duì)應(yīng)的內(nèi)容,為0x64=100毕骡,因?yàn)樯婕暗?strong>字節(jié)序問題削饵,Mac對(duì)應(yīng)的為“小端模式”,驗(yàn)證如下:
int test1_endian() {
    int i = 1;
    char *a = (char *)&i;
    
    if (*a == 1)
        printf("小端\n");
    else
        printf("大端\n");

    return 0;
}

所謂小端模式是一個(gè)數(shù)據(jù)的低位字節(jié)內(nèi)容存在在低地址處未巫,高位字節(jié)內(nèi)容存放在高地址處窿撬,即字節(jié)存儲(chǔ)是順序存儲(chǔ),因此int類型變量i的地址為低地址叙凡;若為大端模式劈伴,即字節(jié)存儲(chǔ)是從高位往低位存儲(chǔ),則變量i的地址為高地址握爷。

上述代碼原理:int類型的1跛璧,在小端模式最低位為1,在大端模式下新啼,最高位為1追城。所以可以通過判斷最低位是否為0來確定該機(jī)器的字節(jié)序是什么;

llvm是一個(gè)完整的編譯器架構(gòu)燥撞,也可以認(rèn)為是一個(gè)用于開發(fā)編譯器座柱、解釋器相關(guān)的庫;clang是一個(gè)c++編寫基于llvb的編譯器叨吮。

llvm工具鏈中的nm工具可顯示目標(biāo)文件的符號(hào)表辆布,或者使用objdmp --syms xxx也可以顯示符號(hào)表;

nm顯示符號(hào)表的類型如下:

  • U茶鉴,未定義符號(hào)
  • A锋玲,絕對(duì)符號(hào),表示該符號(hào)的值的絕對(duì)的涵叮,在以后的鏈接過程中惭蹂,不允許改變,嘗嘗出現(xiàn)在終端向量表中割粮;
  • T盾碗,代碼段的符號(hào),其值表示該符號(hào)在整個(gè)文件當(dāng)中所處的位置舀瓢;
  • D廷雅,定義在數(shù)據(jù)段__data區(qū)中的符號(hào),表明該符號(hào)在初始化數(shù)據(jù)段中;
  • B航缀,定義在數(shù)據(jù)段__bss區(qū)中的符號(hào)商架,表明該符號(hào)位于非初始化數(shù)據(jù)區(qū)中;
  • C芥玉,普通符號(hào)蛇摸,定義在數(shù)據(jù)段__common區(qū)中的符號(hào)
  • I,間接符號(hào)灿巧,表明是另一個(gè)符號(hào)的間接引用
  • S赶袄,其他符號(hào),如代碼段__const區(qū)中的符號(hào)


    image.png

自定義數(shù)據(jù)段數(shù)據(jù)區(qū)抠藕,gcc提供了擴(kuò)展機(jī)制通過關(guān)鍵字__attribute__((section("__DATA,""name")))作為變量屬性加在定義前面饿肺,如:

__attribute__((section("__DATA,""FOO"))) int global = 11;
image.png

mach-o內(nèi)部結(jié)構(gòu)如下圖:

mach-o

  • Header,頭部幢痘,用于快速確認(rèn)該文件的CPU類型唬格、文件類型、加載命令數(shù)量颜说、總字節(jié)大小、標(biāo)志位汰聋;


    mach-o header
  • LoadCommands门粪,加載命令,如何設(shè)置并加載二進(jìn)制數(shù)據(jù)


    LoadCommands加載命令解析
  • Data烹困,數(shù)據(jù)段玄妈,存放數(shù)據(jù)及段信息

  • Loader Info,鏈接信息髓梅,包含了動(dòng)態(tài)加載器用來鏈接可執(zhí)行文件或依賴所需的使用的符號(hào)表拟蜻、字符串表等;


    mach-o內(nèi)部結(jié)構(gòu)選項(xiàng)

數(shù)據(jù)段中包含.eh_frameseciont部分枯饿,解釋如下:
When using languages that support exceptions, such as C++, additional information must be provided to the runtime environment that describes the call frames that much be unwound during the processing of an exception. This information is contained in the special sections .eh_frame and .eh_framehdr.

ELF中的.rel.text重定位表未在mach-o文件中看到酝锅,主要是對(duì)目標(biāo)文件中某些部位進(jìn)行重定位,即代碼段和數(shù)據(jù)段中那些對(duì)絕對(duì)地址的引用的位置奢方,都記錄在該表中搔扁;

趣探 Mach-O:文件格式分析

MachO 文件結(jié)構(gòu)詳解

符號(hào)

對(duì)于不同目標(biāo)文件之間相互“粘合”起來,需要將相互引用的符號(hào)鏈接起來蟋字,其中將函數(shù)和變量統(tǒng)稱為“符號(hào)”稿蹲,函數(shù)名和變量名稱為“符號(hào)名”。

每個(gè)目標(biāo)文件中都會(huì)有一個(gè)相應(yīng)的符號(hào)表(Symbol Table)鹊奖,表中記錄了目標(biāo)文件中所用到的所有符號(hào)苛聘,每個(gè)符號(hào)都有一個(gè)對(duì)應(yīng)的值,稱為“符號(hào)值”,符號(hào)的類型如下:

  • 全局符號(hào)(包括引用外部的全局符號(hào))
  • 段名设哗,有編譯器產(chǎn)生唱捣,其值為該段的起始地址;
  • 局部符號(hào)熬拒,目標(biāo)文件內(nèi)部使用爷光,對(duì)鏈接器來說無用,因此忽略澎粟;
  • 行號(hào)信息蛀序,即目標(biāo)文件指令與源代碼中代碼行的對(duì)應(yīng)關(guān)系;

主要關(guān)注全局符號(hào)活烙,對(duì)于段名徐裸、局部符號(hào)和行號(hào)信息對(duì)其他目標(biāo)文件“不可見”

mac平臺(tái)符號(hào)表如下:

syms

linux平臺(tái)符號(hào)表如下:
linux syms

相比Mac平臺(tái)啸盏,linux下的gcc編譯器下的符號(hào)信息更加詳細(xì)重贺,包含了符號(hào)值、符號(hào)大小回懦、符號(hào)類型和綁定信息气笙、Vis(c/c++中未使用)、Ndx符號(hào)所屬的段怯晕、符號(hào)名稱潜圃;

其中_printf_incre_g_test_a是未定義的舟茶;

ld鏈接器在鏈接腳本中定義的“特殊符號(hào)”(這些符號(hào)也可以使用)谭期,如下:

  • __executable_start,該符號(hào)為程序起始地址吧凉,不是入口地址隧出,是程序最開始的地址;
  • _etext或者 _etext或 _etext阀捅,代碼段結(jié)束地址胀瞪,即代碼段最末尾的地址;
  • _edata或edata也搓,數(shù)據(jù)段結(jié)束地址赏廓,即數(shù)據(jù)段最末尾的地址;
  • _end 或者 end傍妒,程序結(jié)束地址幔摸;

以上地址都為程序被裝載是的虛擬地址;

為了防止符號(hào)名沖突颤练,c語言所有全局變量和函數(shù)經(jīng)過編譯后既忆,符號(hào)名前會(huì)加上下劃線"_";

不過符號(hào)名加下劃線也可以通過gcc編譯器選項(xiàng)-fleading-underscore-fno-leading-underscore來禁用;

對(duì)于c++語言,由于其各種復(fù)雜的特性患雇,如函數(shù)重載跃脊、類、繼承苛吱、虛機(jī)制酪术、命名空間等,會(huì)采專用符號(hào)修飾符號(hào)改編的機(jī)制翠储,如函數(shù)簽名绘雁,即包含了函數(shù)名、參數(shù)類型援所、所在的類和命名空間等庐舟;

函數(shù)簽名

c++filt工具可以用來解析被修飾過的名稱;

強(qiáng)符號(hào)和弱符號(hào)

對(duì)于c/c++語言來說住拭,針對(duì)符號(hào)定義挪略,編譯器默認(rèn)函數(shù)和初始化了的全局變量為“強(qiáng)符號(hào)”,未被初始化的全局變量為“弱符號(hào)”滔岳;可以通過gcc的__attribute__((weak))來定義一個(gè)強(qiáng)符號(hào)為弱符號(hào)杠娱;

針對(duì)強(qiáng)弱符號(hào)概念,鏈接器定義如下規(guī)則來處理強(qiáng)弱符號(hào):

  • 不允許多次定義強(qiáng)符號(hào)谱煤,否則鏈接器報(bào)重復(fù)定義錯(cuò)誤墨辛;
  • 如果一個(gè)符號(hào)在某個(gè)目標(biāo)文件為強(qiáng)符號(hào),其他為弱符號(hào)趴俘,則選擇強(qiáng)符號(hào);
  • 如果一個(gè)符號(hào)在所有目標(biāo)文件都是弱符號(hào)奏赘,則選擇占用空間最大的一個(gè)寥闪;
強(qiáng)引用和弱引用

引用主要用于庫的鏈接過程,通過gcc中的關(guān)鍵字__attribute__((weakref))來定義弱引用磨淌,對(duì)于弱引用疲憋,符號(hào)若未被定義,則鏈接器也不會(huì)報(bào)錯(cuò)梁只,但與之對(duì)應(yīng)的強(qiáng)引用就會(huì)報(bào)”未定義錯(cuò)誤“缚柳;

調(diào)試信息

gcc編譯添加-g選項(xiàng)就可以為目標(biāo)文件加上調(diào)試信息,現(xiàn)在的ELF文件都采用一個(gè)叫做DWARF(Debug With Arbitrary Record Format)的標(biāo)準(zhǔn)調(diào)試信息格式搪锣,可通過strip工具來去除ELF文件中的調(diào)試信息秋忙,具體如下:

strip -S xx.o

靜態(tài)鏈接過程

鏈接器主要是將目標(biāo)文件各個(gè)相同性質(zhì)的段合并,并進(jìn)行地址和空間分配成可執(zhí)行文件的過程构舟。

”地址和空間“是指:

  • 在輸出的可執(zhí)行文件中的空間
  • 裝載后的虛擬地址中的虛擬地址空間

對(duì)于.text.data來說灰追,都需要在可執(zhí)行文件和虛擬地址中分配空間;而對(duì)于.bss這樣的段來說,只局限于虛擬地址空間弹澎,因?yàn)樗鼈冊谖募胁]有內(nèi)容朴下。具體的鏈接過程如下:

  • 空間與地址分配,掃描所有的輸入目標(biāo)文件獲取各個(gè)段的長度苦蒿、屬性和位置殴胧,并將符號(hào)表中所有的符號(hào)定義和符號(hào)引用收集統(tǒng)一放到全局符號(hào)表中,同時(shí)合并所有的同類型段并建立映射關(guān)系佩迟;
  • 符號(hào)解析與重定位团滥,依據(jù)上一步收集的段數(shù)據(jù)及重定位信息,進(jìn)行符號(hào)解析與重定位音五、調(diào)制代碼中的地址等惫撰;

使用ld鏈接為可執(zhí)行文件如下:

ld hello.o hello1.o -L/usr/local/lib -lSystem -o hello

其中-L為指定搜索的庫路徑,-l為指定庫路徑下庫的名稱躺涝;

ld默認(rèn)的程序入口地址為_start厨钻,可通過-e來修改;默認(rèn)的輸出可執(zhí)行文件名為a.out坚嗜;

ld鏈接

如上圖所示夯膀,未鏈接前的目標(biāo)文件代碼段的VMA虛擬地址空間為0x00,因?yàn)樘摂M空間還沒被分配苍蔬;待鏈接后诱建,.text段被分配到了0x10000f00,所有的段都以此為基準(zhǔn)地址碟绑;

對(duì)于段內(nèi)符號(hào)地址俺猿,是段的虛擬地址確定后添加上符號(hào)在段的偏移,即段基址+段內(nèi)偏移格仲;

對(duì)于段外的符號(hào)地址押袍,編譯目標(biāo)文件時(shí)采用臨時(shí)地址的形式交由后續(xù)鏈接來確定實(shí)際虛擬地址,鏈接器根據(jù)符號(hào)的地址對(duì)每個(gè)需要重定位的指令進(jìn)行地址修正凯肋。

具體鏈接器是根據(jù)目標(biāo)文件中的需要重定位的每個(gè)段中的對(duì)應(yīng)的重定位表谊惭,來調(diào)整目標(biāo)文件中段外符號(hào)的地址;

image.png

image.png

上述符號(hào)的地址如何確定侮东?其中之一就是指令修正圈盔,根據(jù)上述符號(hào)的重定位類型分為”相對(duì)尋址修正“和”絕對(duì)尋址修正“;

鏈接器處理多個(gè)相同”弱符號(hào)“但類型不同采用的COMMON塊的機(jī)制(源于Fortan動(dòng)態(tài)分配內(nèi)存的機(jī)制)悄雅,即采用類型占用空間大的符號(hào)為最終的符號(hào)驱敲;對(duì)于其他相同符號(hào)但類型不同的情況,如都是強(qiáng)符號(hào)會(huì)報(bào)錯(cuò)煤伟;一個(gè)強(qiáng)符號(hào)和弱符號(hào)癌佩,以強(qiáng)符號(hào)為準(zhǔn)(若弱符號(hào)所占空間大于強(qiáng)符號(hào)木缝,則警告提示);造成上述采用COMMON塊機(jī)制的原因是鏈接器不支持符號(hào)類型围辙;

image.png

為防止不同類型的符號(hào)采用COMMON塊的形式我碟,則可以通過gcc的-fno-common選項(xiàng)或者__attribut__((nocommon))屬性定義變量來禁用COMMON塊的處理;

不是有-fno-common選項(xiàng)姚建,則弱符號(hào)類型如下:

image.png

使用-fno-common選項(xiàng)矫俺,則弱符號(hào)類型如下:
image.png

靜態(tài)庫

靜態(tài)庫是一組目標(biāo)文件的集合,即很多目標(biāo)文件經(jīng)過壓縮打包(使用ar壓縮工具)后形成的一個(gè)文件掸冤。如linux中最常用的c語言函數(shù)庫libc位于/usr/lib/libc.a厘托,屬于glibc項(xiàng)目的一部分。

gcc -v xxx //使用--verbose選項(xiàng)可打印出詳細(xì)的編譯過程稿湿,簡寫-v

鏈接過程控制

絕大部分情況下铅匹,使用鏈接器默認(rèn)的鏈接規(guī)則就可以完成鏈接任務(wù),但對(duì)于一些特殊要求的程序饺藤,如操作系統(tǒng)包斑、BIOS或嵌入式系統(tǒng)程序,以及一些內(nèi)核驅(qū)動(dòng)程序等涕俗,往往受限于一些特殊的條件罗丰,如須要指定輸出文件的各個(gè)段虛擬地址、段名稱再姑、段存放順序等萌抵,因此需要鏈接控制來控制鏈接過程,一般有如下三種方式:

  • 使用命令行指定鏈接器參數(shù)元镀,如輸出指定目標(biāo)文件名-o绍填、程序啟動(dòng)入口-e
  • 將鏈接指令存放在目標(biāo)文件中栖疑,如VISUAL C++編譯器會(huì)把鏈接參數(shù)放在PE目標(biāo)文件的.drectve段來傳遞參數(shù)沐兰;
  • 使用鏈接控制腳本,這種方式也是最為靈活蔽挠、強(qiáng)大的鏈接控制方法;

linux平臺(tái)下可使用如下指定鏈接控制腳本:

ld -T link.script

具體的詳細(xì)鏈接腳本控制這里不詳細(xì)討論瓜浸,有興趣的自行查閱相關(guān)資料澳淑。

三、裝載與動(dòng)態(tài)鏈接

linux32位系統(tǒng)虛擬內(nèi)存空間布局如下圖:

image.png

可執(zhí)行文件中存在諸多段插佛,且很多段占用空間不足一個(gè)頁杠巡,程序運(yùn)行裝載是為了有效減少虛擬內(nèi)存地址空間的占用,鏈接過程時(shí)就將相同權(quán)限的section節(jié)(如只讀雇寇、可讀可執(zhí)行氢拥、可讀可寫)合并為一個(gè)segment段蚌铜,便于后續(xù)裝載。

但上述相同權(quán)限的合并為一個(gè)segment段嫩海,但段的長度有時(shí)不足以一個(gè)頁的長度冬殃,仍然會(huì)造成內(nèi)存碎片,unix的虛擬內(nèi)存管理通過鄰近段共享一個(gè)物理頁面叁怪。因此审葬,一個(gè)物理頁面可能包含了兩個(gè)段的數(shù)據(jù),甚至可能多于兩個(gè)段(多個(gè)段長度加起來未超過頁的長度)奕谭,段的虛擬內(nèi)存起始地址就可能不是系統(tǒng)頁面長度的整數(shù)倍了涣觉,但里面會(huì)涉及到段地址對(duì)齊,兩個(gè)相鄰段虛擬內(nèi)存起始地址有可能不完全等于起始地址+段長度大小血柳。

棧初始化官册,程序啟動(dòng)后會(huì)將環(huán)境變量及傳入的參數(shù)依次壓入棧,用于后續(xù)main函數(shù)使用难捌;

程序啟動(dòng)過程

創(chuàng)建新進(jìn)場并執(zhí)行新的程序流程如下:

  • 調(diào)用系統(tǒng)調(diào)用函數(shù)fork()創(chuàng)建新進(jìn)程膝宁,其中包含了創(chuàng)建進(jìn)程描述符結(jié)構(gòu)及其內(nèi)部子結(jié)構(gòu)(如內(nèi)存管理結(jié)構(gòu)mm、文件管理結(jié)構(gòu)栖榨、信號(hào)等)并復(fù)制父進(jìn)程的描述符結(jié)構(gòu)昆汹,創(chuàng)建進(jìn)程的內(nèi)核棧并初始化進(jìn)程描述符thread.esp指向新的內(nèi)核棧基地址婴栽,更新進(jìn)程狀態(tài)字段及pid满粗,加入進(jìn)程調(diào)度隊(duì)列等;Linux進(jìn)程描述符task_struct結(jié)構(gòu)體詳解--Linux進(jìn)程的管理與調(diào)度(一) 愚争、《深入理解Linux內(nèi)核》

  • 調(diào)用execve()系統(tǒng)調(diào)用映皆,該系統(tǒng)調(diào)用是glic對(duì)execvp()的包裝,實(shí)際系統(tǒng)調(diào)用的入口是sys_execve()對(duì)參數(shù)進(jìn)行檢查復(fù)制轰枝,然后調(diào)用do_execve()并傳遞環(huán)境變量及輸入?yún)?shù)指針(這些變量位于用戶空間)捅彻,該內(nèi)核函數(shù)會(huì)查找被執(zhí)行文件并讀取文件的前128個(gè)字節(jié),具體是確定文件的類型(通過文件開頭的魔數(shù)magic)鞍陨,根據(jù)文件的類型調(diào)用相應(yīng)的處理函數(shù)薯蝎,如魔數(shù)(占用4個(gè)字節(jié))為0x7F棠隐、elf却紧,則為ELF可執(zhí)行文件汉操;若為解釋型語言淑际,則為#!開頭的解釋器名稱走搁;

  • 根據(jù)文件類型調(diào)用相應(yīng)的裝載函數(shù),對(duì)于ELF為load_elf_binary()筛武,該函數(shù)的具體邏輯如下:

    • 檢查ELF可執(zhí)行文件格式的有效性缝其,比如魔數(shù)挎塌、ELF-Header中段的數(shù)量;
    • 創(chuàng)建用戶態(tài)堆棧并設(shè)置命令行參數(shù)及環(huán)境變量到用戶堆棧内边;
    • 創(chuàng)建內(nèi)存管理結(jié)構(gòu)并根據(jù)ELF可執(zhí)行文件的頭部表描述榴都,對(duì)ELF文件進(jìn)行映射,比如代碼段假残、數(shù)據(jù)段等缭贡;
    • 尋找動(dòng)態(tài)鏈接.interp段,若使用了動(dòng)態(tài)鏈接庫辉懒,就能獲取動(dòng)態(tài)鏈接器路徑阳惹,并使用load_elf_interp()加載其映像;
    • 調(diào)用start_thread()函數(shù)修改保存在內(nèi)核態(tài)堆棧但屬于用戶態(tài)寄存器的eipesp眶俩,使其分別指向動(dòng)態(tài)鏈接程序的入口點(diǎn)和新的用戶堆棧棧頂莹汤;
  • 進(jìn)程系統(tǒng)調(diào)用執(zhí)行iret指令返回時(shí)從內(nèi)核態(tài)轉(zhuǎn)向用戶態(tài),其中會(huì)根據(jù)上一步的eip esp寄存器執(zhí)行動(dòng)態(tài)鏈接程序颠印;

    • 動(dòng)態(tài)鏈接程序在用戶堆棧建立執(zhí)行上下文纲岭,并檢查被執(zhí)行程序以識(shí)別哪些共享庫必須裝載及每個(gè)共享庫中哪個(gè)函數(shù)被有效的請(qǐng)求;
    • 解釋器調(diào)用mmap()系統(tǒng)調(diào)用創(chuàng)建線性區(qū)线罕,并將程序?qū)嶋H使用的庫函數(shù)(正文和數(shù)據(jù))的頁進(jìn)行映射止潮;
    • 解釋器根據(jù)線性區(qū)的線性地址更新對(duì)共享庫符號(hào)的所有引用;
    • 動(dòng)態(tài)鏈接程序跳轉(zhuǎn)到被執(zhí)行程序的主入口函數(shù)_start開始執(zhí)行钞楼;

    Linux進(jìn)程啟動(dòng)過程分析do_execve(可執(zhí)行程序的加載和運(yùn)行)---Linux進(jìn)程的管理與調(diào)度(十一)

動(dòng)態(tài)鏈接

動(dòng)態(tài)鏈接的目的是減少重復(fù)模塊內(nèi)存占用喇闸,并便于升級(jí)動(dòng)態(tài)庫。動(dòng)態(tài)鏈接的過程發(fā)生在程序裝載時(shí)询件,而不是像靜態(tài)鏈接一樣在程序裝載前燃乍;動(dòng)態(tài)鏈接增加了程序運(yùn)行的靈活性,不過也導(dǎo)致程序在性能上的一些損失(相比靜態(tài)鏈接宛琅,大約在5%以下)刻蟹。可執(zhí)行文件的動(dòng)態(tài)符號(hào)是未定義的嘿辟,如下圖:


image.png

對(duì)于共享對(duì)象舆瘪,linux和gcc支持“裝載時(shí)重定位”,即程序裝載時(shí)對(duì)程序模塊中目標(biāo)地址不確定的對(duì)象進(jìn)行重定位红伦;但由于動(dòng)態(tài)庫是共享的介陶,尤其指令部分,重定位時(shí)需要修改指令色建,因此通過對(duì)于動(dòng)態(tài)庫可修改數(shù)據(jù)部分復(fù)制一份副本來解決,即“地址無關(guān)代碼”技術(shù)舌缤。

gcc通過-shared-fPIC選項(xiàng)來支持動(dòng)態(tài)鏈接箕戳,如果只使用-shared某残,那輸出的共享對(duì)象就是使用裝載時(shí)重定位方法,-fPIC實(shí)現(xiàn)地址無關(guān)代碼陵吸;

對(duì)于需要裝載時(shí)確定的變量和函數(shù)訪問玻墅,采用了全局偏移表(Global Offset Table, GOT),即在數(shù)據(jù)段中建立一個(gè)指向這些變量或目標(biāo)函數(shù)的指針數(shù)組壮虫,當(dāng)需要引用變量或調(diào)用目標(biāo)函數(shù)時(shí)澳厢,可以通過GOT中相對(duì)應(yīng)的項(xiàng)間接引用,如下圖所示:

image.png

image.png

動(dòng)態(tài)鏈接性能優(yōu)化

動(dòng)態(tài)鏈接比靜態(tài)鏈接慢的原因是需要對(duì)共享對(duì)象進(jìn)行復(fù)雜的GOT定位囚似,然后間接尋址剩拢,并且是程序啟動(dòng)裝載是進(jìn)行,會(huì)影響程序的啟動(dòng)速度饶唤。

動(dòng)態(tài)鏈接下募狂,存在大量的動(dòng)態(tài)庫的函數(shù)應(yīng)用(其中動(dòng)態(tài)庫的全局變量較少)祸穷,因此程序啟動(dòng)時(shí)需要進(jìn)行函數(shù)引用的符號(hào)查找及重定位性穿,勢必會(huì)增加動(dòng)態(tài)鏈接的運(yùn)行時(shí)間。其實(shí)動(dòng)態(tài)庫中的很多函數(shù)在程序執(zhí)行時(shí)都不會(huì)用到(如一些錯(cuò)誤處理函數(shù)及功能模塊等)雷滚,重定位所有目標(biāo)函數(shù)實(shí)際上是一種浪費(fèi)揭措,因此出現(xiàn)了延遲綁定的技術(shù)胯舷,基本思想是當(dāng)函數(shù)第一次被用到是才進(jìn)行綁定(符號(hào)查找及重定位等),等到需要綁定時(shí)由動(dòng)態(tài)鏈接器來負(fù)責(zé)綁定躬充;并且提供了運(yùn)行時(shí)加載的API逃顶,如打開動(dòng)態(tài)庫dlopen()、查找符號(hào)dlsym()充甚、錯(cuò)誤處理dlerror()及關(guān)閉動(dòng)態(tài)庫dlclose()以政;

在linux下,動(dòng)態(tài)鏈接器ld.so實(shí)際上是一個(gè)共享對(duì)象伴找,操作系統(tǒng)同樣會(huì)通過映射的方式將其加載到進(jìn)程的地址空間盈蛮,并執(zhí)行共享對(duì)象的入口地址;動(dòng)態(tài)鏈接器取得控制權(quán)后會(huì)對(duì)自身進(jìn)程初始化操作技矮,然后根據(jù)當(dāng)前的環(huán)境參數(shù)抖誉,開始對(duì)可執(zhí)行文件鏈接工作殊轴。鏈接完成后,會(huì)將控制權(quán)交到可執(zhí)行文件的入口地址袒炉,程序開始執(zhí)行旁理。

動(dòng)態(tài)鏈接器具體的路徑是在ELF可執(zhí)行文件的.interp段,該段中保存的動(dòng)態(tài)鏈接器路徑字符串我磁;

.interp

.dynamic段保存了動(dòng)態(tài)鏈接器所需要的基本信息孽文,比如依賴于哪些共享對(duì)象、動(dòng)態(tài)鏈接符號(hào)表的位置(.dynsym段)夺艰、動(dòng)態(tài)鏈接重定位表的位置芋哭、共享對(duì)象初始化代碼的地址等;
.dynamic

對(duì)于Mac劲适,通過otool -l hello查看加載命令如下:
image.png

dyld加載時(shí)楷掉,為了優(yōu)化程序啟動(dòng),啟用了共享緩存(shared cache)技術(shù)霞势。共享緩存會(huì)在進(jìn)程啟動(dòng)時(shí)被dyld映射到內(nèi)存中烹植,之后,當(dāng)任何Mach-O映像加載時(shí)愕贡,dyld首先會(huì)檢查該Mach-O映像與所需的動(dòng)態(tài)庫是否在共享緩存中草雕,如果存在,則直接將它在共享內(nèi)存中的內(nèi)存地址映射到進(jìn)程的內(nèi)存地址空間固以。在程序依賴的系統(tǒng)動(dòng)態(tài)庫很多的情況下墩虹,這種做法對(duì)程序啟動(dòng)性能是有明顯提升的,共享緩存是以文件形式存放在/var/db/dyld/目錄下憨琳。

dyld是開源的诫钓,地址為Source Browser

深入理解iOS App的啟動(dòng)過程

dylib動(dòng)態(tài)庫加載過程分析

dyld詳解

linux可通過ldd命令工具查看程序依賴的共享庫,Mac可通過otool -L xx來查看篙螟;

共享庫

linux共享庫命名規(guī)則必須為libname.so.x.y.z菌湃,其中x表示主版本號(hào),y表示次版本號(hào)遍略,z表示發(fā)布版本號(hào)惧所。

主版本號(hào)表示庫的重大升級(jí),不同版本庫是不兼容的绪杏,而次版本號(hào)是對(duì)主版本的增量升級(jí)下愈,如增加了新的接口符號(hào),且保持原來的符號(hào)不變蕾久;發(fā)布版本號(hào)表示庫的一些錯(cuò)誤的修正势似、性能的改進(jìn)等,并不添加任何新的接口,也不會(huì)接口進(jìn)行修改履因。

共享庫的路徑如下:

  • /lib辖佣,主要存放系統(tǒng)最關(guān)鍵和基礎(chǔ)的共享庫,如動(dòng)態(tài)鏈接器搓逾、c語言運(yùn)行庫、數(shù)學(xué)庫等杯拐;
  • /usr/lib霞篡,主要保存非系統(tǒng)運(yùn)行時(shí)關(guān)鍵的共享庫,如開始時(shí)用到的庫端逼,這些庫一般不會(huì)被用戶程序或shell腳本直接使用朗兵;
  • /usr/local/lib,與操作系統(tǒng)無關(guān)的庫顶滩,主要是第三方程序的庫余掖;

動(dòng)態(tài)鏈接器查找共享庫的路徑依據(jù).dynamic段中保存的DT_NEED路徑,來決定是絕對(duì)路徑還是相對(duì)路徑礁鲁,若為相對(duì)路徑盐欺,則去/lib、/usr/lib仅醇、/etc/ld.so.conf配置文件來查找冗美。為加快查找速度,在/etc/ld.so.cache文件中緩存了共享庫析二;

linux可使用環(huán)境變量改變動(dòng)態(tài)鏈接器裝載共享庫路徑的方法粉洼,如下:

  • LD_LIBRARY_PATH,臨時(shí)改變程序的共享庫查找路徑叶摄,其會(huì)改變查找的順序:
    • 環(huán)境變量指定路徑
    • /etc/ld.so.cache指定路徑
    • 默認(rèn)共享庫路徑属韧,先/usr/lib,后/lib蛤吓;
  • LD_PRELOAD宵喂,指定預(yù)先裝載的一些共享庫或目標(biāo)文件(比指定目錄裝載的還要優(yōu)先),可用于覆蓋后續(xù)加載的同名全局符號(hào)來改寫庫的某些函數(shù)柱衔,對(duì)程序調(diào)試或測試非常有用樊破;
  • LD_DEBUG,打開動(dòng)態(tài)鏈接器調(diào)試功能唆铐,動(dòng)態(tài)鏈接器會(huì)打印各種有用的信息哲戚;

內(nèi)存

linux進(jìn)程地址空間布局如下圖:


linux進(jìn)程地址空間布局

棧保存了一個(gè)函數(shù)調(diào)用所需要的維護(hù)信息,稱為“堆棧幀”或“活動(dòng)記錄”艾岂,一般包括:

  • 函數(shù)的返回地址和參數(shù)

  • 臨時(shí)變量:包括函數(shù)的非靜態(tài)局部變量以及編譯器自動(dòng)生成的其他臨時(shí)變量

  • 保存的上下文:包括在函數(shù)調(diào)用前后需要保持不變的寄存器



    函數(shù)體的標(biāo)準(zhǔn)開頭一般是這樣的:

  • push ebp顺少,把ebp壓入棧中

  • mov ebp, espebp = esp,這時(shí)ebp指向棧頂脆炎;

  • sub esp, xxx梅猿,在棧上分配xxx字節(jié)的臨時(shí)空間

  • push xxx,保存寄存器(主要是函數(shù)返回時(shí)恢復(fù)以前的狀態(tài))

函數(shù)返回時(shí):

  • pop xxx秒裕,恢復(fù)保存過的寄存器
  • mov esp, ebp袱蚓,恢復(fù)esp同時(shí)回收局部變量空間
  • pop ebp,從棧中恢復(fù)保存的ebp
  • ret几蜻,從棧中取得返回地址喇潘,并跳轉(zhuǎn)到該位置;

進(jìn)程的堆空間管理完全交由操作系統(tǒng)來管理梭稚,會(huì)造成每次都需要進(jìn)行系統(tǒng)調(diào)用颖低,嚴(yán)重影響程序性能;而是采用程序向操作系統(tǒng)申請(qǐng)一塊適當(dāng)大小的對(duì)空間弧烤,然后由程序自己管理這塊空間忱屑,因此涉及到堆的分配算法。

linux堆管理如下:

linux堆管理

linux 內(nèi)核維護(hù)一個(gè)break指針暇昂,這個(gè)指針指向堆空間的某個(gè)地址莺戒。從堆起始地址(Heap’s Start)到break之間的地址空間為映射好的(虛擬地址與物理地址的映射,通過MMU實(shí)現(xiàn))话浇,可以供進(jìn)程訪問脏毯;而從break往上,是未映射的地址空間幔崖,如果訪問這段空間則程序會(huì)報(bào)錯(cuò)食店。所以,如果Mapped Region 空間不夠時(shí)赏寇,會(huì)調(diào)整break指針吉嫩,擴(kuò)大映射空間,重新分配內(nèi)存嗅定。

linux分配堆空間的分配方式:brk()系統(tǒng)調(diào)用自娩,mmap()內(nèi)存映射;

int brk(void* end_data_segment)

brk()作用實(shí)際上是設(shè)置進(jìn)程數(shù)據(jù)段的結(jié)束地址渠退,即它可以擴(kuò)大或者縮小數(shù)據(jù)段(linux下數(shù)據(jù)段和bss合并一起統(tǒng)稱為“數(shù)據(jù)段”)忙迁;glibc還有一個(gè)函數(shù)sbrk(),其功能與brk()類似碎乃,只不過參數(shù)和返回值略有不同姊扔,實(shí)際上是brk()的包裝。

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

mmap作用是向操作系統(tǒng)申請(qǐng)一段虛擬內(nèi)存地址空間梅誓,該空間可以映射到某個(gè)文件恰梢,或者不映射作為“匿名空間”使用佛南。

glibc中的malloc函數(shù)具體處理用戶的空間請(qǐng)求如下:

  • 對(duì)于小于128KB的請(qǐng)求,會(huì)從現(xiàn)有堆空間按照堆分配算法分配嵌言;
  • 對(duì)于大于128KB的請(qǐng)求嗅回,會(huì)使用mmap函數(shù)分配一塊匿名空間,然后再這個(gè)匿名空間為用戶分配空間摧茴;

C語言內(nèi)存管理:malloc绵载、calloc、free的實(shí)現(xiàn)

linux-malloc底層實(shí)現(xiàn)原理

對(duì)于如何管理一大塊連續(xù)的內(nèi)存空間苛白,涉及到堆分配算法尘分,主要是如下:

  • 空閑鏈表,將各個(gè)空閑的塊按照鏈表的方式連接起來丸氛,當(dāng)用戶請(qǐng)求一塊空間時(shí),可以遍歷整個(gè)鏈表著摔,知道找到合適大小的塊并拆分缓窜;當(dāng)釋放時(shí)將其合并到空閑鏈表中;

    空閑鏈表損壞導(dǎo)致整個(gè)堆無法使用谍咆,且搜索鏈表效率較低禾锤;

  • 位圖,將內(nèi)存空間劃分為大量塊(block)摹察,每個(gè)塊大小相同恩掷,第一個(gè)塊稱為已分配區(qū)域的頭(Head),其余的稱為已分配的主體(Body)供嚎,每個(gè)塊存在頭/主體/空閑三種狀態(tài)黄娘,并用整數(shù)數(shù)組來記錄塊的使用情況;

    速度快克滴,穩(wěn)定性好(為避免用戶越界破壞數(shù)據(jù)逼争,可簡單備份下位圖),易于管理劝赔。但若塊設(shè)置大小太大誓焦,會(huì)造成內(nèi)存碎片;若太小着帽,會(huì)導(dǎo)致位圖占用空間大杂伟,可采用多級(jí)位圖緩解。

  • 內(nèi)存池仍翰,針對(duì)實(shí)際使用中被分配對(duì)象的大小是較為固定的幾個(gè)值赫粥,因此可以按照每次請(qǐng)求分配的大小作為一個(gè)單位來劃分堆空間,塊的管理可以采用空閑鏈表或者位圖歉备;C++ 內(nèi)存池介紹與經(jīng)典內(nèi)存池的實(shí)現(xiàn)

運(yùn)行庫

典型的程序運(yùn)行步驟如下:

  • 操作系統(tǒng)創(chuàng)建進(jìn)程后傅是,把控制權(quán)交到了程序的入口,這個(gè)入口往往是運(yùn)行庫中的某個(gè)入口函數(shù);
  • 入口函數(shù)對(duì)運(yùn)行庫和程序運(yùn)行環(huán)境進(jìn)行初始化喧笔,包括堆帽驯、I/O、線程书闸、全局變量構(gòu)造等尼变;
  • 入口函數(shù)完成初始化后嫌术,調(diào)用main函數(shù),正式執(zhí)行程序主體部分牌借;
  • main函數(shù)執(zhí)行完畢后度气,返回到入口函數(shù),入口函數(shù)進(jìn)行清理工作膨报,包括全局變量析構(gòu)磷籍、堆銷毀、關(guān)閉I/O等现柠,然后進(jìn)行系統(tǒng)調(diào)用結(jié)束進(jìn)程院领。

上文中“程序啟動(dòng)流程”中已經(jīng)闡述動(dòng)態(tài)鏈接程序會(huì)最終執(zhí)行被執(zhí)行程序的主入口函數(shù),其為_start够吩,具體原因是ld鏈接時(shí)被鏈接控制腳本鏈接到程序.text段的起始位置比然;

程序入口

image.png

鏈接控制腳本中設(shè)置_start入口函數(shù)代碼:
鏈接器腳本

具體的_start函數(shù)執(zhí)行流如下:

_start -> __libc_start_main -> main -> exit

__libc_start_main()函數(shù)定義如下:

int LIBC_START_MAIN
    (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
    int argc,
    char **argv,
    __typeof (main) init,
    void (*fini) (void),
    void (*rtld_fini) (void),
    void *stack_end)

其實(shí)現(xiàn)關(guān)鍵函數(shù)調(diào)用如下:

__pthread_initialize_minimal();
__cxa_atexit(rtld_fini, NULL, NULL);
__libc_init_first(argc, argv, __environ);
__cxa_atexit(fini, NULL, NULL);
(*init)(argc, argv, __environ);
result = main(argc, argv, __environ);
exit(result);

具體的工作就是建立運(yùn)行環(huán)境并設(shè)置一系列的main函數(shù)執(zhí)行前后的處理函數(shù)指針,包括argc/argv入棧周循,初始化線程環(huán)境强法,注冊main函數(shù)的退出處理函數(shù)指針,初始化libc湾笛,

init初始化(該函數(shù)位于.init段)拟烫,執(zhí)行main函數(shù),main函數(shù)結(jié)束后調(diào)用exit函數(shù)迄本,該函數(shù)會(huì)調(diào)用注冊的退出處理函數(shù)硕淑,進(jìn)程正常退出;

其中_start匯編代碼中末尾指令為hlt嘉赎,該指令表示強(qiáng)行停止程序置媳,為了防止程序未調(diào)用exit函數(shù)(該函數(shù)執(zhí)行不會(huì)返回,直接進(jìn)程退出公条,因此正常情況下不會(huì)執(zhí)行hlt指令)拇囊。

用戶空間的程序啟動(dòng)過程

Mac dyld進(jìn)程啟動(dòng)

首先明確一個(gè)概念:”進(jìn)程地址空間布局隨機(jī)化(Address Space Layout Randomization, ASLR)“,是一種避免APP被攻擊的有效保護(hù)靶橱。

采用ASLR技術(shù)寥袭,進(jìn)程每次啟動(dòng)時(shí)路捧,地址空間都會(huì)被簡單地隨機(jī)化——只是偏移,不是攪亂传黄。實(shí)現(xiàn)方式是通過內(nèi)核將Mach-O的“平移”某個(gè)隨機(jī)數(shù)杰扫。

iOS上我們可以直接使用dyld.h中的方法_dyld_get_image_vmaddr_slide來獲取image虛擬地址的偏移量。

進(jìn)程擁有私有的內(nèi)存地址空間膘掰,傳統(tǒng)的方式章姓,進(jìn)程啟動(dòng)加載都是按照固定可預(yù)見的方式,即進(jìn)程鏡像在虛擬內(nèi)存地址的固定的识埋,且進(jìn)程運(yùn)行生命周期內(nèi)凡伊,大部分內(nèi)存分配的操作都是按照相同的方式,因此進(jìn)程在內(nèi)存的地址分布具有非常強(qiáng)的可預(yù)測性窒舟。

這給黑客提供了更大的施展空間系忙。黑客主要采用的方法是代碼注入:通過重寫內(nèi)存中的函數(shù)指針,黑客就可以將程序的執(zhí)行路徑轉(zhuǎn)到自己的代碼惠豺,將程序的輸入變?yōu)樽约旱妮斎氡棵佟V貙憙?nèi)存最常用的方法是采用緩沖區(qū)溢出(即利用未經(jīng)保護(hù)的內(nèi)存復(fù)制操作越過棧上數(shù)組的邊界),將函數(shù)的返回地址重寫為自己的指針耕腾。此外,任何用戶指針甚至結(jié)構(gòu)化的異常處理程序都可以導(dǎo)致代碼注入杀糯。這里的關(guān)鍵問題在于判斷重寫哪些指針扫俺,也就是說,可靠地判斷注入的代碼應(yīng)該在內(nèi)存中的什么位置固翰。

Mach-O文件介紹之ASLR(進(jìn)程地址空間布局隨機(jī)化)

image.png

dyld動(dòng)態(tài)鏈接器的入口為__dyld_start狼纬,其中存在bl函數(shù)跳轉(zhuǎn)指令跳轉(zhuǎn)到dyldbootstrap::start(),該函數(shù)主要工作是:

  • rebaseDyld()骂际,內(nèi)核ASLR偏移來重定位dyld
  • mach_init()疗琉,mach消息初始化,以使用mach消息
  • __guard_setup歉铝,棧溢出保護(hù)
  • dyld::main()盈简,執(zhí)行dyld主體函數(shù)

具體main主體函數(shù)流程如下:

  • 設(shè)置運(yùn)行環(huán)境

    主要是設(shè)置運(yùn)行參數(shù)、環(huán)境變量等太示,如mach-o頭部結(jié)構(gòu)體及ASLR偏移量柠贤,并調(diào)用setContext()設(shè)置上下文,包括dyld的回調(diào)函數(shù)类缤、參數(shù)(如argc臼勉、argv、envp等)及一些標(biāo)志信息餐弱;配置進(jìn)程的受限模式(默認(rèn)開啟受限模式并代碼簽名)宴霸;檢測并設(shè)置環(huán)境變量囱晴;獲取架構(gòu)信息;

  • 加載共享緩存

    iOS是必須要開啟共享緩存瓢谢,其共享緩存路徑為/System/Library/Caches/com.apple.dyld/dyld_shared_cache_armX畸写,通過mmap進(jìn)行映射并重定位;

  • 實(shí)例化主程序

    主要是將主程序的mach-o加載進(jìn)內(nèi)存恩闻,并實(shí)例化一個(gè)ImageLoader對(duì)象;

  • 加載插入的動(dòng)態(tài)庫

    加載環(huán)境變量DYLD_INSERT_LIBRARIES設(shè)置的動(dòng)態(tài)庫艺糜;

  • 鏈接主程序

    調(diào)用link()函數(shù)將實(shí)例化的主程序進(jìn)行動(dòng)態(tài)修正,讓二進(jìn)制變?yōu)榭烧?zhí)行的狀態(tài)幢尚;

  • 鏈接插入的動(dòng)態(tài)庫

  • 執(zhí)行弱符號(hào)綁定

  • 執(zhí)行初始化方法

    其中包含了類加載+load()以及全局c++/c對(duì)象的構(gòu)造函數(shù)破停;

  • 查找主程序的入口并返回

    從加載命令LC_MAIN讀取入口,如無則使用LC_UNIXTHREAD讀取入口尉剩,并返回入口函數(shù)地址真慢;

可以設(shè)置DYLD_PRINT_STATISTICS_DETAILS環(huán)境變量來打印應(yīng)用的啟動(dòng)時(shí)間,如下圖:

程序啟動(dòng)時(shí)間統(tǒng)計(jì)

dyld調(diào)試選項(xiàng)

具體詳細(xì)的關(guān)于dyld的使用理茎,可通過man dyld查看黑界。

系統(tǒng)調(diào)用

linux系統(tǒng)調(diào)用是通過0x80中斷完成,各個(gè)通用寄存器用于傳遞參數(shù)皂林,其中eax寄存器用于系統(tǒng)調(diào)用的接口號(hào)朗鸠,如eax=1表示退出進(jìn)程(exit),eax=2表示創(chuàng)建進(jìn)程(fork)础倍,eax=3表示讀取文件或I/O(read)烛占,eax=4表示寫文件或I/O(write)等,每個(gè)系統(tǒng)調(diào)用都對(duì)應(yīng)內(nèi)核代碼中的函數(shù)沟启,以sys_開頭忆家。

具體的系統(tǒng)調(diào)用實(shí)現(xiàn)是通過函數(shù)庫封裝的_syscall()中的int $0x80匯編指令執(zhí)行,并通過eax及其他通用寄存器來傳遞參數(shù)德迹;中斷發(fā)生后芽卿,CPU根據(jù)操作系統(tǒng)建立的系統(tǒng)調(diào)用中斷描述符轉(zhuǎn)入內(nèi)核態(tài),并切換到內(nèi)核棧并保存用戶態(tài)進(jìn)程的寄存器到內(nèi)核棧胳搞,同時(shí)跳轉(zhuǎn)到具體的系統(tǒng)調(diào)用函數(shù)執(zhí)行卸例,執(zhí)行系統(tǒng)調(diào)用服務(wù)后,會(huì)執(zhí)行IRET指令肌毅,該指令會(huì)回到用戶態(tài)并恢復(fù)內(nèi)核棧保存的用戶態(tài)寄存器轉(zhuǎn)入用戶態(tài)币厕,此時(shí)繼續(xù)執(zhí)行eip執(zhí)行的int $0x80指令后的指令繼續(xù)執(zhí)行,直至系統(tǒng)調(diào)用完成芽腾。

實(shí)用工具

  • vmmap旦装,用于顯示進(jìn)程的虛擬內(nèi)存使用

  • vm_stat,用于顯示內(nèi)核內(nèi)部的虛擬內(nèi)存統(tǒng)計(jì)摊滔;

    其中涉及到虛擬內(nèi)存的一些概念:


    頁面狀態(tài)

附錄測試程序

hello.c

#include <stdio.h>
#include "hello1.h"

int g_int_a = 1;
static float s_float_a;
const int c = 100;
const char *name __attribute__((section("__DATA," "FOO"))) = "hello";

extern int incre_g_int_a(void);

int main(int argc, char **argv) {
    int a = 1;
    incre_g_int_a();
    printf("g_int_a:%d, a:%d", g_int_a, a);

    return 0;
}

hello1.h

int incre_g_int_a(void);

hello1.c

#include "hello1.h"

extern int g_int_a;

int incre_g_int_a(void) {
    g_int_a++;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阴绢,一起剝皮案震驚了整個(gè)濱河市店乐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呻袭,老刑警劉巖眨八,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異左电,居然都是意外死亡廉侧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門篓足,熙熙樓的掌柜王于貴愁眉苦臉地迎上來段誊,“玉大人,你說我怎么就攤上這事栈拖×幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵涩哟,是天一觀的道長索赏。 經(jīng)常有香客問我,道長贴彼,這世上最難降的妖魔是什么潜腻? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮器仗,結(jié)果婚禮上融涣,老公的妹妹穿的比我還像新娘。我一直安慰自己青灼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布妓盲。 她就那樣靜靜地躺著杂拨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悯衬。 梳的紋絲不亂的頭發(fā)上弹沽,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音筋粗,去河邊找鬼策橘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娜亿,可吹牛的內(nèi)容都是我干的丽已。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼买决,長吁一口氣:“原來是場噩夢啊……” “哼沛婴!你這毒婦竟也來了吼畏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤嘁灯,失蹤者是張志新(化名)和其女友劉穎泻蚊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丑婿,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡性雄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了羹奉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秒旋。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖尘奏,靈堂內(nèi)的尸體忽然破棺而出滩褥,到底是詐尸還是另有隱情,我是刑警寧澤炫加,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布瑰煎,位于F島的核電站,受9級(jí)特大地震影響俗孝,放射性物質(zhì)發(fā)生泄漏酒甸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一赋铝、第九天 我趴在偏房一處隱蔽的房頂上張望插勤。 院中可真熱鬧,春花似錦革骨、人聲如沸农尖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盛卡。三九已至,卻和暖如春筑凫,著一層夾襖步出監(jiān)牢的瞬間滑沧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工巍实, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滓技,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓棚潦,卻偏偏與公主長得像令漂,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355