匯編學(xué)習(xí)筆記
花了一周的時(shí)間匙监,對(duì)匯編做了一次深刻的復(fù)習(xí)和再學(xué)習(xí),想記下來(lái)的東西有很多小作,我盡量把總結(jié)寫(xiě)全亭姥。
0x01.8086匯編
個(gè)人認(rèn)為學(xué)習(xí)匯編從8086入手對(duì)之后的匯編理解有非常大的幫助,這時(shí)候很想推薦一本書(shū)顾稀,王爽的《匯編語(yǔ)言》
达罗,深入淺出地講解了8086匯編,我自己也是從頭刷了一遍静秆,對(duì)匯編的世界有了更清楚的認(rèn)識(shí)粮揉。總的來(lái)說(shuō)抚笔,我認(rèn)為入門(mén)匯編要學(xué)的東西扶认,真的不多,因?yàn)樗械暮瘮?shù)調(diào)用來(lái)來(lái)回回都是這么點(diǎn)東西殊橙,接下來(lái)我會(huì)做一些總結(jié)
0x011.總線(xiàn)
總線(xiàn)分為地址總線(xiàn)辐宾、數(shù)據(jù)總線(xiàn)和控制總線(xiàn),上圖是CPU從內(nèi)存中讀取數(shù)據(jù)的過(guò)程膨蛮,它們很形象地反映了關(guān)于他們的各自的職責(zé)螃概。當(dāng)然每根線(xiàn)都他們各自的算法,這里不展開(kāi)鸽疾,網(wǎng)上有很多基礎(chǔ)資料吊洼。
0x012.寄存器
我們都知道當(dāng)CPU做大部分運(yùn)算的時(shí)候,都是將內(nèi)存的值讀到CPU中制肮,就是存在我們所說(shuō)的寄存器
中冒窍,再在CPU中進(jìn)行加減乘除與或非這些基本的邏輯運(yùn)算张吉。所有的程序都是基于這成千上萬(wàn)次上述的操作榛泛。
8086CPU有14個(gè)寄存器:AX除盏、BX味混、CX、DX谬莹、SI檩奠、DI、SP附帽、BP埠戳、IP、CS蕉扮、SS整胃、DS、ES喳钟、PSW都是16位屁使。這其中又分為通用寄存器和段寄存器、標(biāo)志寄存器等奔则。
AX蛮寂、BX、CX易茬、DX這四個(gè)被稱(chēng)為通過(guò)寄存器共郭,他們主要是存儲(chǔ)數(shù)據(jù)的,因?yàn)槭?6位的疾呻,所有只有"可憐"的2字節(jié)可以存。這其中又分為高八位和低八位写半,主要是為了空間的合理利用岸蜗。
而段寄存器包括CS(Code segment)和IP(偏移)合成指令的物理地址、DS(Data segment)存放要訪(fǎng)問(wèn)數(shù)據(jù)的段地址叠蝇、SS(Stack segment)和SP(偏移)璃岳。另外一個(gè)BP寄存器一般用來(lái)存放棧的基址來(lái)配合操作棧
。
標(biāo)志寄存器(flag)記錄當(dāng)前指令結(jié)果的狀態(tài)悔捶。
零標(biāo)志位(Zero Flag)
如果結(jié)果為0铃慷,那么zf = 1(表示結(jié)果是0);如果結(jié)果不為0蜕该,那么zf = 0犁柜。
奇偶標(biāo)志位 (PF)
,如果1的個(gè)數(shù)為偶數(shù)堂淡,pf = 1馋缅,如果為奇數(shù)扒腕,那么pf = 0。
符號(hào)標(biāo)志位(Symbol Flag)
萤悴,記錄其結(jié)果是否為負(fù)瘾腰。如果結(jié)果為負(fù),sf = 1覆履;如果非負(fù)蹋盆,sf = 0。
進(jìn)位標(biāo)志位(CF)
在進(jìn)行無(wú)符號(hào)數(shù)運(yùn)算
的時(shí)候硝全,它記錄了運(yùn)算結(jié)果的最高有效位向更高位的進(jìn)位值栖雾,或從更高位的借位值。
溢出標(biāo)志位(OF)
,記錄了有符號(hào)數(shù)運(yùn)算的結(jié)果是否發(fā)生了溢出柳沙。如果發(fā)生溢出岩灭,OF = 1;如果沒(méi)有赂鲤,OF = 0噪径。
0x013.棧
棧的開(kāi)口一般是向上的,棧底為高地址数初,棧頂為低地址找爱,開(kāi)棧的操作是從高地址往低地址走。
總的來(lái)說(shuō)泡孩,上圖紅線(xiàn)間隔區(qū)域就是一個(gè)函數(shù)調(diào)用棧
也稱(chēng)為一個(gè)棧幀
车摄,即使是現(xiàn)在方法調(diào)用的實(shí)現(xiàn)原理也依然是大同小異。在一個(gè)棧幀當(dāng)中仑鸥,包含了函數(shù)所需要的環(huán)境吮播。比如參數(shù)、局部變量眼俊、以及返回地址(回家的路)意狠。函數(shù)和函數(shù)之間就是這樣一段接著一段進(jìn)行執(zhí)行。首先參數(shù)入棧疮胖,該參數(shù)是下個(gè)函數(shù)調(diào)用需要使用的參數(shù)环戈,然后記錄方法下一條的執(zhí)行地址,方便方法返回后繼續(xù)執(zhí)行澎灸,之后用BP存一下棧幀的基地址和拉伸SP開(kāi)啟棧幀內(nèi)部的域院塞,之后在函數(shù)體內(nèi)的局部變量都存在這個(gè)棧幀中,如果還有下一個(gè)方法那么繼續(xù)入?yún)⒑捅4娣祷氐刂分貜?fù)上述操作性昭。如果方法調(diào)用完畢拦止,所有的環(huán)境都會(huì)被POP完,回到上一個(gè)函數(shù)繼續(xù)執(zhí)行糜颠,所以這也就是為什么局部變量過(guò)了方法创泄,再訪(fǎng)問(wèn)就會(huì)出現(xiàn)很大的危險(xiǎn)艺玲,因?yàn)樵撚蛞呀?jīng)是一個(gè)釋放掉的域。
PS:在開(kāi)發(fā)過(guò)程中經(jīng)常碰到Stack over flow(棧溢出)就是因?yàn)殚_(kāi)發(fā)者不停地在開(kāi)棧幀鞠抑,卻得不到釋放饭聚,棧的空間被用光了,比如你寫(xiě)了一個(gè)沒(méi)有邊界限制的遞歸搁拙,反正我日常開(kāi)發(fā)能不寫(xiě)遞歸就不寫(xiě)秒梳,如果寫(xiě)遞歸我會(huì)尤其要注意邊界的條件,及時(shí)return箕速。我一直覺(jué)得遞歸本身效率就不高酪碘,只是代碼看起來(lái)易讀、易理解罷了盐茎。
0x014.棧平衡
棧平衡又分為外平棧和內(nèi)平棧兴垦,其實(shí)沒(méi)有什么特別的東西,就是當(dāng)函數(shù)調(diào)用完畢之后需要把SP拉回來(lái)
字柠,否則棧遲早會(huì)被用完探越、因?yàn)樗械拈_(kāi)棧操作都是通過(guò)SP為基址進(jìn)行的。內(nèi)平棧就是在棧幀內(nèi)部做平衡窑业,外平棧就是函數(shù)調(diào)用完進(jìn)行平棧钦幔。
0x02.ARM64
之后來(lái)到現(xiàn)代社會(huì),看看如今的arm64和8086的區(qū)別
0x021.寄存器
ARM64也擁有有31個(gè)64位的通用寄存器 x0 到 x30,他們也是通常用來(lái)存儲(chǔ)數(shù)據(jù)常柄,w0 到 w28 這些是32位的. 因?yàn)?4位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.比如 w0 就是 x0的低32位!
PC寄存器(program counter)為指令指針寄存器鲤氢,它指示了CPU當(dāng)前要讀取指令的地址
浮點(diǎn)寄存器 64位: D0 - D31 32位: S0 - S31,向量寄存器 128位:V0-V31
SP寄存器其實(shí)是X31寄存器西潘,他在任意時(shí)刻會(huì)保存我們棧頂?shù)牡刂?
(是不是和8086的SP一模一樣)
FP寄存器也稱(chēng)為x29寄存器屬于通用寄存器,一般我們利用它保存棧底的地址(是不是和8086的BP很像)
x30寄存器也稱(chēng)為L(zhǎng)R寄存器卷玉,存放的是函數(shù)的返回地址.當(dāng)ret指令執(zhí)行時(shí)刻,會(huì)尋找x30寄存器保存的地址值!
ARM64同樣有狀態(tài)寄存器,這和8086大同小異喷市。
0x022.高速緩存
CPU每執(zhí)行一條指令前都需要從內(nèi)存中將指令讀取到CPU內(nèi)并執(zhí)行相种。而寄存器的運(yùn)行速度相比內(nèi)存讀寫(xiě)要快很多,為了性能,CPU開(kāi)始有了一個(gè)高速緩存存儲(chǔ)區(qū)域.當(dāng)程序在運(yùn)行時(shí),先將要執(zhí)行的指令代碼以及數(shù)據(jù)復(fù)制到高速緩存中去(由操作系統(tǒng)完成).CPU直接從高速緩存依次讀取指令來(lái)執(zhí)行东抹,iPhoneX上搭載的ARM處理器A11它的1級(jí)緩存的容量是64KB,2級(jí)緩存的容量8M沃测。
0x023.關(guān)于內(nèi)存讀寫(xiě)指令
注意:讀/寫(xiě) 數(shù)據(jù)是都是往高地址讀/寫(xiě)
str(store register)指令
將數(shù)據(jù)從寄存器中讀出來(lái),存到內(nèi)存中.
ldr(load register)指令
將數(shù)據(jù)從內(nèi)存中讀出來(lái),存到寄存器中
此ldr 和 str 的變種ldp 和 stp 還可以操作2個(gè)寄存器.
0x024.函數(shù)調(diào)用棧
關(guān)于函數(shù)調(diào)用時(shí)對(duì)棧的處理其實(shí)基本是和8086差不多缭黔,只要懂了8086匯編,理解起ARM64函數(shù)調(diào)用就非常容易了蒂破,只是在規(guī)則上略有不同馏谨。
- 一般來(lái)說(shuō) arm64上 x0 - x7 分別會(huì)存放方法的前 8 個(gè)參數(shù)。
- 如果參數(shù)個(gè)數(shù)超過(guò)了8個(gè)附迷,多余的參數(shù)會(huì)存在棧上惧互,新方法會(huì)通過(guò)棧來(lái)讀取哎媚。因此少量的參數(shù)可以幫助我們提升性能,CPU計(jì)算喊儡,肯定比先讀內(nèi)存再計(jì)算來(lái)的快拨与。
- 方法的返回值一般都在 x0 上。
- 如果方法返回值是一個(gè)較大的數(shù)據(jù)結(jié)構(gòu)時(shí)艾猜,結(jié)果會(huì)存在 x8 執(zhí)行的地址上买喧。
0x03.思考與感悟
下面聊一下反思與感悟。
0x031.ARM64在進(jìn)行棧拉伸的時(shí)候到底是如何計(jì)算的
開(kāi)始的時(shí)候我一直很不理解的一件事就是:它是怎么知道每個(gè)棧需要開(kāi)辟多少空間的匆赃?比如我們?cè)诜椒▋?nèi)部用到的局部變量一多淤毛,他就會(huì)開(kāi)辟多一些的棧空間(因?yàn)樾枰止?jié)對(duì)齊的緣故算柳,基本是以0x10低淡、0x20、0x30..規(guī)則拉伸)瞬项,當(dāng)入?yún)⒆兌鄷r(shí)蔗蹋,他同樣會(huì)開(kāi)辟多一些的空間,即使你在內(nèi)部沒(méi)有用到滥壕,還有當(dāng)遇到可變參數(shù)時(shí)纸颜,他的規(guī)則又是怎么樣的。找了很久绎橘,終于在公司內(nèi)網(wǎng)找到了匯編大牛指引了方向胁孙。其實(shí)在arm的開(kāi)發(fā)者官網(wǎng)上都有說(shuō)明 https://developer.arm.com/documentation/ihi0055/latest/ ,英文不好的同學(xué)可以查看譯文arm64程序調(diào)用規(guī)則称鳞。
總而言之涮较,言而總之。對(duì)于調(diào)用者而言冈止,即使他認(rèn)為在調(diào)用之前已經(jīng)分配了足夠的堆椏衿保空間來(lái)容納的參數(shù),但實(shí)際上熙暴,只有在參數(shù)封裝處理之后才能知道所需的堆椆胧簦空間量到底需要多少。(因?yàn)闀?huì)牽涉到指針變量周霉、結(jié)構(gòu)體掂器、可變參數(shù)等等)。因此參數(shù)傳遞過(guò)程大致分為3個(gè)階段:
-
階段A – 初始化 (在開(kāi)始處理參數(shù)之前俱箱,該階段僅執(zhí)行一次)
- NGRN = 0
- NSRN = 0
- NSAA = SP(NSAA設(shè)置為當(dāng)前的SP)
-
階段B - 預(yù)填充和擴(kuò)展參數(shù) (把參數(shù)列表中的每一個(gè)參數(shù)国瓮,去匹配下面規(guī)則,第一個(gè)被匹配到的規(guī)則,應(yīng)用到該參數(shù)上乃摹。如果沒(méi)有規(guī)匹配到規(guī)則禁漓,那么參數(shù)不修改)
- 如果參數(shù)類(lèi)型是復(fù)合類(lèi)型,調(diào)用者和被調(diào)用者都不能確定其大小孵睬,則將參數(shù)復(fù)制到內(nèi)存中播歼,并將參數(shù)替換為指向該內(nèi)存的指針。 (C / C ++語(yǔ)言中沒(méi)有這樣的類(lèi)型肪康,其它語(yǔ)言存在荚恶。)
- 如果參數(shù)是HFA或HVA類(lèi)型,則參數(shù)不修改磷支。
- 如果參數(shù)是大于16個(gè)字節(jié)的復(fù)合類(lèi)型谒撼,調(diào)用者申請(qǐng)一個(gè)內(nèi)存,將參數(shù)復(fù)制到內(nèi)存里去雾狈,并將參數(shù)替換為指向該內(nèi)存的指針廓潜。
- 如果參數(shù)是復(fù)合類(lèi)型,則參數(shù)的大小向上舍入為最接近8個(gè)字節(jié)的倍數(shù)善榛。(例如參數(shù)大小為9字節(jié)辩蛋,修改為16字節(jié))
-
階段C- 把參數(shù)放到寄存器或棧里 (參數(shù)列表中的每個(gè)參數(shù),將依次應(yīng)用以下規(guī)則移盆,直到參數(shù)放到寄存器或棧里悼院,此參數(shù)處理完成,然后再?gòu)膮?shù)列表中取參數(shù)咒循。注: 將參數(shù)分配給寄存器時(shí)据途,寄存器中未使用的位的值不確定。 將參數(shù)分配給棧時(shí)叙甸,未填充字節(jié)的值不確定颖医。)
- (1) 如果參數(shù)是half(16bit),single(16bit)裆蒸,double(32bit)或quad(64bit)浮點(diǎn)數(shù)或Short Vector Type熔萧,并且NSRN小于8,則將參數(shù)放入寄存器v[NSRN]的最低有效位僚祷。 NSRN增加1佛致。 此參數(shù)處理完成。
- (2) 如果參數(shù)是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)類(lèi)型辙谜,且NSRN + (HFA或HVA成員個(gè)數(shù)) ≤ 8俺榆,則每個(gè)成員依次放入SIMD and Floating-point 寄存器,NSRN=NSRN+ HFA或HVA成員個(gè)數(shù)筷弦。此參數(shù)處理完成肋演。
- (3) 如果參數(shù)是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)類(lèi)型,但是NSRN已經(jīng)等于8(說(shuō)明v0-v7被使用完畢)烂琴。則參數(shù)的大小向上舍入為最接近8個(gè)字節(jié)的倍數(shù)爹殊。(例如參數(shù)大小為9字節(jié),修改為16字節(jié))
- (4) 如果參數(shù)是HFA(homogeneous floating-point aggregate)奸绷、HVA(homogeneous short vector aggregate)梗夸、quad(64bit)浮點(diǎn)數(shù)或Short Vector Type,NSAA = NSAA+max(8, 參數(shù)自然對(duì)齊大小)号醉。
- (5) 如果參數(shù)是half(16bit)反症,single(16bit)浮點(diǎn)數(shù),參數(shù)擴(kuò)展到8字節(jié)(放入最低有效位畔派,其余bits值不確定)
- (6) 如果參數(shù)是HFA(homogeneous floating-point aggregate)铅碍、HVA(homogeneous short vector aggregate)、half(16bit)线椰,single(16bit)胞谈,double(32bit)或quad(64bit)浮點(diǎn)數(shù)或Short Vector Type,參數(shù)copy到內(nèi)存憨愉,NSAA=NSAA+size(參數(shù))烦绳。此參數(shù)處理完成。
- (7) 如果參數(shù)是整型或指針類(lèi)型配紫、size(參數(shù))<=8字節(jié)径密,且NGRN小于8,則參數(shù)復(fù)制到x[NGRN]中的最低有效位躺孝。 NGRN增加1享扔。 此參數(shù)處理完成。
- (8) 如果參數(shù)對(duì)齊后16字節(jié)括细,NGRN向上取偶數(shù)伪很。(例如:NGRN為2,那值保持不變奋单;假如NGRN為3锉试,則取4。 注:iOS ABI沒(méi)有這個(gè)規(guī)則)
- (9) 如果參數(shù)是整型览濒,對(duì)齊后16字節(jié)呆盖,且NGRN小于7,則把參數(shù)復(fù)制到x[NGRN] 和 x[NGRN+1]贷笛,x[NGRN]是低位应又。NGRN = NGRN + 2。 此參數(shù)處理完成乏苦。
- (10) 如果參數(shù)是復(fù)合類(lèi)型株扛,且參數(shù)可以完全放進(jìn)x寄存器(8-NGRN>= 參數(shù)字節(jié)大小/8)尤筐。從x[NGRN]依次放入?yún)?shù)(低位開(kāi)始)。未填充的bits的值不確定洞就。NGRN = NGRN + 此參數(shù)用掉的寄存器個(gè)數(shù)盆繁。此參數(shù)處理完成。
- (11) NGRN設(shè)為8旬蟋。
- (12) NSAA = NSAA+max(8, 參數(shù)自然對(duì)齊大小)油昂。
- (13) 如果參數(shù)是復(fù)合類(lèi)型,參數(shù)copy到內(nèi)存倾贰,NSAA=NSAA+size(參數(shù))冕碟。此參數(shù)處理完成。
- (14) 如果參數(shù)小于8字節(jié)匆浙,參數(shù)設(shè)置為8字節(jié)大小安寺,高位bits值不確定。
- (15) 參數(shù)copy到內(nèi)存首尼,NSAA=NSAA+size(參數(shù))我衬。此參數(shù)處理完成。
從上面規(guī)則饰恕,可以得到經(jīng)驗(yàn):
- 處理完參數(shù)列表中所有的參數(shù)后挠羔,調(diào)用者通過(guò)特定的規(guī)則一定知道傳遞參數(shù)用了多少棧空間埋嵌。(NSAA - SP)
- 浮點(diǎn)數(shù)和short vector types通過(guò)v寄存器和棧傳遞破加,不會(huì)通過(guò)r寄存器傳遞。(除非是小復(fù)合類(lèi)型的成員)
- 寄存器和棧中雹嗦,參數(shù)未填充滿(mǎn)的部分的值范舀,不可確定。
0x032.AT&T匯編和 Intel匯編的區(qū)別
AT&T 就是我們?cè)陂_(kāi)發(fā)的ARM系列的匯編了罪,Intel匯編就是我們說(shuō)的x86匯編锭环,其實(shí)在實(shí)現(xiàn)思路上,他們大致還是相同的泊藕。只是在指令集和寄存器的設(shè)計(jì)上略有不同辅辩。
所以Xcode模擬器(Intel)和我們的真機(jī)調(diào)試(ARM)還是有一些略微不同,在調(diào)試的時(shí)候也要略微的注意語(yǔ)法娃圆。比如MOV的賦值操作玫锋,操作數(shù)和被操作數(shù)需要反一反。
0x033.msgSend的Hook
有了之前的基礎(chǔ)之后讼呢,我們可以用匯編來(lái)做一些很trick的事撩鹿,比如在iOS的開(kāi)發(fā)過(guò)程中,我們可以hook objc的msgSend
進(jìn)行一些耗時(shí)方法的統(tǒng)計(jì)悦屏。其實(shí)msgSend
本身是可以用fishhook
去做hook的节沦,我們知道像msgsend
這類(lèi)的系統(tǒng)函數(shù)键思,是需要在啟動(dòng)時(shí)動(dòng)態(tài)綁定的(和NSlog
一樣),因此可以成功hook甫贯。難點(diǎn)在于稚机,msgSend的入?yún)⑹强勺儏?shù),還有是否有返回值获搏、返回值類(lèi)型等諸多問(wèn)題,所以用常見(jiàn)的開(kāi)發(fā)語(yǔ)言是無(wú)法實(shí)現(xiàn)此類(lèi)的函數(shù)的失乾,所以他的實(shí)現(xiàn)本身就是用匯編實(shí)現(xiàn)的常熙,不能當(dāng)簡(jiǎn)單的函數(shù)hook來(lái)處理。但是要強(qiáng)行hook還是能做到的碱茁,github也已經(jīng)有較現(xiàn)成的方法:https://github.com/czqasngit/objc_msgSend_hook裸卫,在這里講一下作者大致的思路。
1.首先使用fishhook
去hook msgsend
纽竣,用hook_objc_msgSend
去替換origin_objc_msgSend
墓贿。
2.在hook_objc_msgSend
第一步首先我們需要保存函數(shù)的環(huán)境,所以將x0-x7依次入棧蜓氨。
3.這時(shí)候作者是開(kāi)始執(zhí)行了自己的函數(shù)(beforeCall)聋袋,比如當(dāng)前時(shí)間打點(diǎn),保存類(lèi)名信息等等穴吹,之后通過(guò)線(xiàn)程私有共享緩存進(jìn)行保存幽勒。
4.依次將寄存器數(shù)據(jù)出棧,恢復(fù)環(huán)境港令,進(jìn)行原始方法origin_objc_msgSend
的調(diào)用啥容。
5.重復(fù)2,3的操作顷霹,執(zhí)行自己的函數(shù)(afterCall)咪惠,比如記錄方法的耗時(shí)操作等等。
6.調(diào)用ret淋淀,繼續(xù)執(zhí)行X30中的函數(shù)地址遥昧,讓程序繼續(xù)執(zhí)行下去。
以上就是該作者的大致思路朵纷,實(shí)現(xiàn)上確實(shí)不難渠鸽,能想到怎么實(shí)現(xiàn)才是真的難。
類(lèi)似這樣的實(shí)現(xiàn)思路還能做很多事柴罐,比如hook全局的block徽缚,思路差不多,先用fishhook hook系統(tǒng)函數(shù)革屠,然后用匯編處理函數(shù)環(huán)境 比如這位大佬 https://github.com/youngsoft/YSBlockHook凿试。
0x04.總結(jié)
通過(guò)本次的學(xué)習(xí)排宰,對(duì)匯編的實(shí)現(xiàn)有了新的認(rèn)識(shí),也學(xué)到了用匯編去幫助我們?nèi)?shí)現(xiàn)一些技術(shù)方案那婉,收獲也是頗豐板甘。其實(shí)當(dāng)我們深入去看日常的匯編代碼的時(shí)候,我們會(huì)發(fā)現(xiàn)日常接觸到的所有函數(shù)详炬,搞來(lái)搞去就這么點(diǎn)操作盐类,入棧出棧、存儲(chǔ)讀取等等呛谜,只要熟練掌握規(guī)則在跳,其實(shí)看匯編真的只是煩,但他不難隐岛!
五一頹廢了猫妙,5天基本都在玩,沒(méi)學(xué)習(xí)聚凹,擠出這么點(diǎn)學(xué)習(xí)成果自我安慰下割坠。
0x05.引用
https://github.com/czqasngit/objc_msgSend_hook