現(xiàn)在高級(jí)語(yǔ)言”橫行“,閱讀匯編的能力似乎已經(jīng)不那么重要。但當(dāng)我們查看 core dump和有時(shí)候追求極致性能的時(shí)候,匯編還是要讀一讀。本文嘗試以閱讀簡(jiǎn)單匯編為出發(fā)點(diǎn),闡述一些基本技巧。 本文是如何看懂程序Crash系列之一。 跟高級(jí)語(yǔ)言相比,匯編晦澀難懂。但是它們是有規(guī)律可循的,并且有技巧可以幫忙。只要明白這些規(guī)律和技巧,閱讀基本的匯編很容易。下面讓我們來(lái)看看這些規(guī)律和技巧是什么。 匯編是什么 機(jī)器只能讀懂二進(jìn)制指令,而匯編是一組特定的字符,它們映射到二進(jìn)制指令,用于方便記憶和編寫(xiě)二進(jìn)制指令。比如move rax, rdx就是我們常見(jiàn)的匯編。匯編指令經(jīng)過(guò)匯編器(assembler)轉(zhuǎn)變成二進(jìn)制指令。 現(xiàn)在高級(jí)語(yǔ)言會(huì)編譯到匯編的有C/C++, Go, Rust。其他比如Java,C#,會(huì)編譯到虛擬機(jī)指令而不是直接的匯編。(虛擬機(jī)指令與匯編有許多共同的地方) 什么時(shí)候會(huì)跟匯編打交道 當(dāng)我們調(diào)試release版本的時(shí)候,debug symbols已經(jīng)被去掉了或者根本沒(méi)有。 當(dāng)程序crash的時(shí)候,只有一個(gè)core dump,而且crash的地方不明所以。 當(dāng)我們想要研究語(yǔ)言的一個(gè)高級(jí)特性性能如何。 等等,這時(shí)候我們都要深入研究一下匯編,去讀懂匯編背后的邏輯。 下面讓我們先看幾個(gè)簡(jiǎn)單的匯編例子。 簡(jiǎn)單的匯編示例 通過(guò)簡(jiǎn)單的匯編感受一下匯編。 mov rbp, rsp 將寄存器rsp的值存儲(chǔ)到寄存器rbp中。 mov DWORD PTR [rbp-4], 4將四個(gè)字節(jié)的4存儲(chǔ)到地址為rbp-4的棧上。(什么是4個(gè)字節(jié)的4?就是0x00000004,大小為四個(gè)字節(jié)) sub rsp, 16將rsp的值減去16。 上面的匯編格式Intel的語(yǔ)法。常見(jiàn)的匯編有兩種語(yǔ)法,一種是Intel,另一種是AT & T。Intel的格式是 opcode destination, source,類似于語(yǔ)法 int i = 4;而AT & T的格式是opcode source, destination,直觀理解為 move from source to destination。 上面Intel的匯編,如果改寫(xiě)成AT & T,則為 movq %rsp, %rbp movl $4, -4(%rbp) subq $16, %rsp AT & T的匯編另外一個(gè)特點(diǎn)是有前綴比如%,$。指令還有后綴q,l,等等。這些前綴后綴有特殊的意思,后文會(huì)講解。不同的格式側(cè)重點(diǎn)不太一樣,你可以選擇你喜歡的格式。 如何一通百通匯編 閱讀匯編的關(guān)鍵點(diǎn)是函數(shù)調(diào)用結(jié)構(gòu),數(shù)據(jù)傳輸方式,常見(jiàn)控制結(jié)構(gòu),具體指令功能。 函數(shù)調(diào)用結(jié)構(gòu) 讓我們以下面的簡(jiǎn)單代碼的為例,
看看它對(duì)應(yīng)的匯編
sqaure和main前面的push rbp 和mov rbp, rsp又叫做函數(shù)的序言(prologue),幾乎每個(gè)函數(shù)一開(kāi)始都會(huì)有的指令。它和函數(shù)最后的pop rbp和ret(epilogue)起到維護(hù)函數(shù)的調(diào)用棧的作用。首先讓我們看看什么是函數(shù)的調(diào)用棧。 程序都是一個(gè)函數(shù)(稱為caller)調(diào)用另外一個(gè)函數(shù)(稱為callee),這么嵌套下去(callee在調(diào)用其他函數(shù)的時(shí)候,自己就變成了caller,其他函數(shù)是它的callee)。 為了在執(zhí)行完callee的時(shí)候可以跳轉(zhuǎn)回調(diào)用的地方(caller調(diào)用callee的下一個(gè)指令),程序會(huì)以棧的方式維護(hù)著函數(shù)的調(diào)用關(guān)系。具體是,每個(gè)函數(shù)都會(huì)對(duì)應(yīng)一個(gè)frame(棧幀),這個(gè)frame包含了用于恢復(fù)到caller的信息和當(dāng)前函數(shù)用于計(jì)算的數(shù)據(jù)(又稱局部變量)。 見(jiàn)下圖,這些用于恢復(fù)的信息,包含返回地址,caller(previous frame)的rbp。(注意順序是先push 了返回地址,然后是rbp,如下圖灰色和綠色的框框。)函數(shù)調(diào)用的時(shí)候,callee的frame就會(huì)疊加在已有的frame上面,像一個(gè)盤(pán)子放在另外一個(gè)盤(pán)子上面,形成調(diào)用棧。藍(lán)色背景是callee的frame,橙色背景是caller的frame。 (注意:棧是向著低地址的方向生長(zhǎng)) 函數(shù)的調(diào)用棧,是理解匯編的第一道坎。第二道坎是函數(shù)的調(diào)用習(xí)慣(calling convention)也就是函數(shù)參數(shù)的存儲(chǔ)和傳遞方式。為了理解第二道坎,我們要先看看數(shù)據(jù)的傳遞。 數(shù)據(jù)的傳遞 函數(shù)在計(jì)算的時(shí)候,存儲(chǔ)數(shù)據(jù)的地方總共有三個(gè),寄存器,內(nèi)存和程序本身。寄存器的個(gè)數(shù)和名字取決于具體的計(jì)算機(jī)架構(gòu),本文以x86-64為例子。內(nèi)存分為棧空間和堆(heap)空間,靜態(tài)區(qū)。程序本身是指只讀的程序數(shù)據(jù)片段,比如int i = 4,這個(gè)4存儲(chǔ)于程序本身,在匯編里面又叫立即數(shù)(immediate number)。 知道了數(shù)據(jù)的存儲(chǔ)地方,那么數(shù)據(jù)的傳遞就分為以下四個(gè)方面
注意:數(shù)據(jù)不能從內(nèi)存直接傳遞到內(nèi)存。如果需要從內(nèi)存?zhèn)鬟f到內(nèi)存,要以寄存器為中介。(這些知識(shí),還是我當(dāng)年大學(xué)學(xué)的計(jì)算機(jī)組成原理里面的) 數(shù)據(jù)是有大小的,比如一個(gè)word是兩個(gè)字節(jié),double words是四個(gè)字節(jié)。所以傳遞數(shù)據(jù)的時(shí)候,要知道傳遞的數(shù)據(jù)大小。Intel的匯編會(huì)在數(shù)據(jù)前面說(shuō)明數(shù)據(jù)大小,比如 mov DWORD PTR [rbp-4], 4,意思是將一個(gè)4字節(jié)的4存儲(chǔ)到 棧上(地址為rbp-4)。而AT & T是通過(guò)指令的后綴來(lái)說(shuō)明,同樣的指令為movl $4, -4(%rbp)。而存儲(chǔ)的地方,AT & T匯編是通過(guò)前綴來(lái)區(qū)別,比如%q前綴表示寄存器,$表示立即數(shù),()表示內(nèi)存。 了解了數(shù)據(jù)的傳遞方式,那么讓我們看看函數(shù)的調(diào)用習(xí)慣。 函數(shù)的調(diào)用習(xí)慣(calling convention) caller調(diào)用callee,要將參數(shù)(arguements)傳遞給callee。一個(gè)函數(shù)可以接收多個(gè)參數(shù),而caller與callee之間約定的每個(gè)參數(shù)的應(yīng)該怎么傳遞就是調(diào)用習(xí)慣。這樣子,callee就會(huì)到指定的位置獲取相應(yīng)的參數(shù)。 比如一開(kāi)始的main調(diào)用square。參數(shù)i如何傳遞到square里面?通過(guò)閱讀上面的匯編,我們可以知道在main里面,4先存到棧上,然后存在edi里面,而sqaure函數(shù)直接從edi里面讀取4的值。這說(shuō)明了,參數(shù)4是通過(guò)寄存器edi傳給了calle (sqaure) 。可能有讀者會(huì)以為,從代碼看,參數(shù)不是直接就傳給了sqaure嗎。實(shí)際上,在匯編,這個(gè)變量i是不存在的,只有寄存器和內(nèi)存。我們需要約定好i的值存在哪里。 下面讓我們看看這些約定:常見(jiàn)寄存器負(fù)責(zé)傳遞的參數(shù)以及一些作用 注意:
有眼尖的讀者會(huì)發(fā)現(xiàn),匯編1里面,第一個(gè)參數(shù)是用edi來(lái)傳遞的,為什么這里是rdi?因?yàn)閞di是8字節(jié)的,4字節(jié)的時(shí)候?qū)?yīng)的就是edi。 如果函數(shù)返回比較大的對(duì)象,那么第一個(gè)參數(shù)rdi會(huì)用來(lái)傳遞存儲(chǔ)這個(gè)對(duì)象的地址。這個(gè)地址是由caller分配的。有了這些基礎(chǔ),那么你就可以理解C++里面的copy elision了,可以挑戰(zhàn)一下Copy/move elision: C++ 17 vs C++ 11 常見(jiàn)控制結(jié)構(gòu) 這個(gè)對(duì)于入門(mén)的程序員很容易理解。控制結(jié)構(gòu)就是if, while循環(huán)等等。在匯編里面,它們都是基于判定語(yǔ)句,跳轉(zhuǎn)語(yǔ)句:做一個(gè)計(jì)算,檢查相應(yīng)的flag,然后根據(jù)flag的值確定要跳轉(zhuǎn)到哪里。比如下面的If語(yǔ)句
對(duì)應(yīng)的匯編如下,cmpl $6, -8(%rbp) 根據(jù)對(duì)比結(jié)果,修改對(duì)應(yīng)的標(biāo)志位。下一行匯編jle .L4檢查對(duì)應(yīng)的標(biāo)志位,如果less and equal to 6,那么就跳轉(zhuǎn)到.L4,如果不是,就繼續(xù)執(zhí)行。
指令 對(duì)于指令,可以直接搜索得知具體的指令的作用,所以就不一一介紹了。講講一點(diǎn)小竅門(mén)。 CMP destination, source;JBE .L3是指如果destination <= source則跳轉(zhuǎn)到.L3。 技巧 Compiler Explorer這個(gè)網(wǎng)站會(huì)顯示代碼對(duì)應(yīng)的匯編并且進(jìn)行了相應(yīng)的顏色匹配,非常方便查看匯編。而且鼠標(biāo)點(diǎn)擊相應(yīng)的匯編還會(huì)告訴提示,比如這個(gè)匯編是干什么的。所以我們可以借助這個(gè)網(wǎng)站來(lái)閱讀匯編。 實(shí)際的例子 查看crash的地方 下面我將用gdb一步一步探索當(dāng)初在產(chǎn)品里的core dump。寫(xiě)下來(lái),也是為了以后我再次遇到相似的問(wèn)題可以有參考。 某天,產(chǎn)品crash了,生成了core dump, 于是我們可以用命令gdb <exec> <core>來(lái)加載core dump。 加載完core dump以后,截取crash附近的匯編如下
我們可以看到crash的地方是move (%rax), %rdx,那么我們查看寄存器rax的內(nèi)容,發(fā)現(xiàn)是0。加上這個(gè)core dump是segment fault,那么crash的理由大概是訪問(wèn)了空指針。 接著,我們看看rax是怎么來(lái)的。上一條指令是call *0x110(%rax),猜想是訪問(wèn)虛函數(shù),想知道直覺(jué)是怎么來(lái)的,請(qǐng)看怎么理解C++虛函數(shù)?fat pointer in GO/Rust vs thin pointer in C++ 。 那么我們可以看看這個(gè)虛函數(shù)是什么。首先要知道現(xiàn)在rax的值。根據(jù)mov 0x0(%rbp), %rax,我們可以知道,rax等于rbp存儲(chǔ)的值,所以用下面的命令查看rbp存儲(chǔ)的內(nèi)容
接著,我們計(jì)算虛函數(shù)的地址為:p/x 0x110+$rax = p/x 0x110 + 0x00007ff95dc5f308 得到地址為0x00007ff95dc5f308,接著就可以查看這個(gè)地址存儲(chǔ)的虛函數(shù)是什么 (x/gx 0x7ff95dc5f418),發(fā)現(xiàn)是GetStream。所以我們可以知道GetStream返回了空指針。接下來(lái)我們就要查看產(chǎn)品代碼看看為什么會(huì)返回空指針。如果是正常的空指針,那么說(shuō)明crash的地方要檢查指針,如果不是正常的情況,那么我們就要修相應(yīng)的地方。 全部的命令放到一塊就是
已知虛指針,打印虛函數(shù)表
練習(xí)題 題目一、請(qǐng)找出下面函數(shù)void g(s* p,int j)的參數(shù)傳遞
源碼是
題目二、找出下面Sum參數(shù)傳遞的寄存器或者棧
答案是下圖 參考文獻(xiàn) 《高效C/C++調(diào)試》清華大學(xué)出版社 X86-64 Architecture Guide http://www.cs./~evans/cs216/guides/x86.html#calling https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf |
|
來(lái)自: imnobody2001 > 《Linux pgm》