一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

X86-64寄存器和棧幀

 xinyz4104 2014-04-28

X86-64寄存器和棧幀

概要

說(shuō)到x86-64,總不免要說(shuō)說(shuō)AMD的牛逼,x86-64是x86系列中集大成者,繼承了向后兼容的優(yōu)良傳統(tǒng),最早由AMD公司提出,代號(hào)AMD64;正是由于能向后兼容,AMD公司打了一場(chǎng)漂亮翻身戰(zhàn)。導(dǎo)致Intel不得不轉(zhuǎn)而生產(chǎn)兼容AMD64的CPU。這是IT行業(yè)以弱勝?gòu)?qiáng)的經(jīng)典戰(zhàn)役。不過(guò),大家為了名稱延續(xù)性,更習(xí)慣稱這種系統(tǒng)結(jié)構(gòu)為x86-64
X86-64在向后兼容的同時(shí),更主要的是注入了全新的特性,特別的:x86-64有兩種工作模式,32位OS既可以跑在傳統(tǒng)模式中,把CPU當(dāng)成i386來(lái)用;又可以跑在64位的兼容模式中,更加神奇的是,可以在32位的OS上跑64位的應(yīng)用程序。有這種好事,用戶肯定買(mǎi)賬啦,
值得一提的是,X86-64開(kāi)創(chuàng)了編譯器的新紀(jì)元,在之前的時(shí)代里,Intel CPU的晶體管數(shù)量一直以摩爾定律在指數(shù)發(fā)展,各種新奇功能層出不窮,比如:條件數(shù)據(jù)傳送指令cmovg,SSE指令等。但是GCC只能保守地假設(shè)目標(biāo)機(jī)器的CPU是1985年的i386,額。。。這樣編譯出來(lái)的代碼效率可想而知,雖然GCC額外提供了大量?jī)?yōu)化選項(xiàng),但是這對(duì)應(yīng)用程序開(kāi)發(fā)者提出了很高的要求,會(huì)者寥寥。X86-64的出現(xiàn),給GCC提供了一個(gè)絕好的機(jī)會(huì),在新的x86-64機(jī)器上,放棄保守的假設(shè),進(jìn)而充分利用x86-64的各種特性,比如:在過(guò)程調(diào)用中,通過(guò)寄存器來(lái)傳遞參數(shù),而不是傳統(tǒng)的堆棧。又如:盡量使用條件傳送指令,而不是控制跳轉(zhuǎn)指令

寄存器簡(jiǎn)介

先明確一點(diǎn),本文關(guān)注的是通用寄存器(后簡(jiǎn)稱寄存器)。既然是通用的,使用并沒(méi)有限制;后面介紹寄存器使用規(guī)則或者慣例,只是GCC(G++)遵守的規(guī)則。因?yàn)槲覀兿雽?duì)GCC編譯的C(C++)程序進(jìn)行分析,所以了解這些規(guī)則就很有幫助。
在體系結(jié)構(gòu)教科書(shū)中,寄存器通常被說(shuō)成寄存器文件,其實(shí)就是CPU上的一塊存儲(chǔ)區(qū)域,不過(guò)更喜歡使用標(biāo)識(shí)符來(lái)表示,而不是地址而已。
X86-64中,所有寄存器都是64位,相對(duì)32位的x86來(lái)說(shuō),標(biāo)識(shí)符發(fā)生了變化,比如:從原來(lái)的%ebp變成了%rbp。為了向后兼容性,%ebp依然可以使用,不過(guò)指向了%rbp的低32位。
X86-64寄存器的變化,不僅體現(xiàn)在位數(shù)上,更加體現(xiàn)在寄存器數(shù)量上。新增加寄存器%r8到%r15。加上x(chóng)86的原有8個(gè),一共16個(gè)寄存器。
剛剛說(shuō)到,寄存器集成在CPU上,存取速度比存儲(chǔ)器快好幾個(gè)數(shù)量級(jí),寄存器多了,GCC就可以更多的使用寄存器,替換之前的存儲(chǔ)器堆棧使用,從而大大提升性能。
讓寄存器為己所用,就得了解它們的用途,這些用途都涉及函數(shù)調(diào)用,X86-64有16個(gè)64位寄存器,分別是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。其中:

  • %rax 作為函數(shù)返回值使用。
  • %rsp 棧指針寄存器,指向棧頂
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函數(shù)參數(shù),依次對(duì)應(yīng)第1參數(shù),第2參數(shù)。。。
  • %rbx,%rbp,%r12,%r13,%14,%15 用作數(shù)據(jù)存儲(chǔ),遵循被調(diào)用者使用規(guī)則,簡(jiǎn)單說(shuō)就是隨便用,調(diào)用子函數(shù)之前要備份它,以防他被修改
  • %r10,%r11 用作數(shù)據(jù)存儲(chǔ),遵循調(diào)用者使用規(guī)則,簡(jiǎn)單說(shuō)就是使用之前要先保存原值

棧幀

棧幀結(jié)構(gòu)

C語(yǔ)言屬于面向過(guò)程語(yǔ)言,他最大特點(diǎn)就是把一個(gè)程序分解成若干過(guò)程(函數(shù)),比如:入口函數(shù)是main,然后調(diào)用各個(gè)子函數(shù)。在對(duì)應(yīng)機(jī)器語(yǔ)言中,GCC把過(guò)程轉(zhuǎn)化成棧幀(frame),簡(jiǎn)單的說(shuō),每個(gè)棧幀對(duì)應(yīng)一個(gè)過(guò)程。X86-32典型棧幀結(jié)構(gòu)中,由%ebp指向棧幀開(kāi)始,%esp指向棧頂。

函數(shù)進(jìn)入和返回

函數(shù)的進(jìn)入和退出,通過(guò)指令call和ret來(lái)完成,給一個(gè)例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include
#include </code>
  
int foo ( int x )
{
int array[] = {1,3,5};
return array[x];
}       /* -----  end of function foo  ----- */
  
int main ( int argc, char *argv[] )
{
int i = 1;
int j = foo(i);
fprintf(stdout, "i=%d,j=%d\n", i, j);
return EXIT_SUCCESS;
}               /* ----------  end of function main  ---------- */

命令行中調(diào)用gcc,生成匯編語(yǔ)言:

1
Shell > gcc –S –o test.s test.c

Main函數(shù)第40行的指令Call foo其實(shí)干了兩件事情:

  • Pushl %rip //保存下一條指令(第41行的代碼地址)的地址,用于函數(shù)返回繼續(xù)執(zhí)行
  • Jmp foo //跳轉(zhuǎn)到函數(shù)foo

Foo函數(shù)第19行的指令ret 相當(dāng)于:

  • popl %rip //恢復(fù)指令指針寄存器

棧幀的建立和撤銷(xiāo)

還是上一個(gè)例子,看看棧幀如何建立和撤銷(xiāo)
說(shuō)題外話,以”點(diǎn)”做為前綴的指令都是用來(lái)指導(dǎo)匯編器的命令。無(wú)意于程序理解,統(tǒng)統(tǒng)忽視之,比如第31行。
棧幀中,最重要的是幀指針%ebp和棧指針%esp,有了這兩個(gè)指針,我們就可以刻畫(huà)一個(gè)完整的棧幀
函數(shù)main的第30~32行,描述了如何保存上一個(gè)棧幀的幀指針,并設(shè)置當(dāng)前的指針。
第49行的leave指令相當(dāng)于:
Movq %rbp %rsp //撤銷(xiāo)棧空間,回滾%rsp
Popq %rbp //恢復(fù)上一個(gè)棧幀的%rbp
同一件事情會(huì)有很多的做法,GCC會(huì)綜合考慮,并作出選擇。選擇leave指令,極有可能因?yàn)樵撝噶钚枰鎯?chǔ)空間少,需要時(shí)鐘周期也少。
你會(huì)發(fā)現(xiàn),在所有的函數(shù)中,幾乎都是同樣的套路,
我們通過(guò)gdb觀察一下進(jìn)入foo函數(shù)之前main的棧幀,進(jìn)入foo函數(shù)的棧幀,退出foo的棧幀情況

1
2
3
4
Shell> gcc -g -o test test.c 
Shell> gdb --args test
Gdb > break main
Gdb > run

進(jìn)入foo函數(shù)之前:

你會(huì)發(fā)現(xiàn)rbp-rsp=0×20,這個(gè)是由代碼第11行造成的。
進(jìn)入foo函數(shù)的棧幀:

回到main函數(shù)的棧幀,rbp和rsp恢復(fù)成進(jìn)入foo之前的狀態(tài),就好像什么都沒(méi)發(fā)生一樣。

可有可無(wú)的幀指針

你剛剛搞清楚幀指針,是不是很期待要馬上派上用場(chǎng),這樣你可能要大失所望,因?yàn)榇蟛糠值某绦颍技恿藘?yōu)化編譯選項(xiàng):-O2,這幾乎是普遍的選擇。在這種優(yōu)化級(jí)別,甚至更低的優(yōu)化級(jí)別-O1,都已經(jīng)去除了幀指針,也就是%ebp中再也不是保存幀指針,而且另作他途。
在x86-32時(shí)代,當(dāng)前棧幀總是從保存%ebp開(kāi)始,空間由運(yùn)行時(shí)決定,通過(guò)不斷push和pop改變當(dāng)前棧幀空間;x86-64開(kāi)始,GCC有了新的選擇,優(yōu)化編譯選項(xiàng)-O1,可以讓GCC不再使用棧幀指針,下面引用 gcc manual 一段話 :

1
-O also turns on -fomit-frame-pointer on machines where doing so does not interfere with debugging.

這樣一來(lái),所有空間在函數(shù)開(kāi)始處就預(yù)分配好,不需要棧幀指針;通過(guò)%rsp的偏移就可以訪問(wèn)所有的局部變量。
說(shuō)了這么多,還是看看例子吧。同一個(gè)例子, 加上-O1選項(xiàng):

1
Shell>: gcc –O1 –S –o test.s test.c 

分析main函數(shù),GCC分析發(fā)現(xiàn)棧幀只需要8個(gè)字節(jié),于是進(jìn)入main之后第一條指令就分配了空間(第23行):

1
Subq $8, %rsp

然后在返回上一棧幀之前,回收了空間(第34行):

1
Addq $8, %rsp 

等等,為啥main函數(shù)中并沒(méi)有對(duì)分配空間的引用呢?這是因?yàn)镚CC考慮到棧幀對(duì)齊需求,故意做出的安排。
再來(lái)看foo函數(shù),這里你可以看到%rsp是如何引用??臻g的。
等等,不是需要先預(yù)分配空間嗎?這里為啥沒(méi)有預(yù)分配,直接引用棧頂之外的地址?
這就要涉及x86-64引入的牛逼特性了。

訪問(wèn)棧頂之外

通過(guò)readelf查看可執(zhí)行程序的header信息:

紅色區(qū)域部分指出了x86-64遵循ABI規(guī)則的版本,它定義了一些規(guī)范,遵循ABI的具體實(shí)現(xiàn)應(yīng)該滿足這些規(guī)范,其中,他就規(guī)定了程序可以使用棧頂之外128字節(jié)的地址。
這說(shuō)起來(lái)很簡(jiǎn)單,具體實(shí)現(xiàn)可有大學(xué)問(wèn),這超出了本文的范圍,具體大家參考虛擬存儲(chǔ)器。別的不提,接著上例,我們發(fā)現(xiàn)GCC利用了這個(gè)特性,干脆就不給foo函數(shù)分配棧幀空間了,而是直接使用棧幀之外的空間。@恨少說(shuō)這就相當(dāng)于內(nèi)聯(lián)函數(shù)唄,我要說(shuō):這就是編譯優(yōu)化的力量。

寄存器保存慣例

過(guò)程調(diào)用中,調(diào)用者棧幀需要寄存器暫存數(shù)據(jù),被調(diào)用者棧幀也需要寄存器暫存數(shù)據(jù)。如果調(diào)用者使用了%rbx,那被調(diào)用者就需要在使用之前把%rbx保存起來(lái),然后在返回調(diào)用者棧幀之前,恢復(fù)%rbx。遵循該使用規(guī)則的寄存器就是被調(diào)用者保存寄存器,對(duì)于調(diào)用者來(lái)說(shuō),%rbx就是非易失的。
反過(guò)來(lái),調(diào)用者使用%r10存儲(chǔ)局部變量,為了能在子函數(shù)調(diào)用后還能使用%r10,調(diào)用者把%r10先保存起來(lái),然后在子函數(shù)返回之后,再恢復(fù)%r10。遵循該使用規(guī)則的寄存器就是調(diào)用者保存寄存器,對(duì)于調(diào)用者來(lái)說(shuō),%r10就是易失的,
舉個(gè)例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
  
void sfact_helper ( long int x, long int * resultp)
{
    if (x<=1)
        *resultp = 1;
    else {
        long int nresult;
        sfact_helper(x-1,&nresult);
        *resultp = x * nresult;
    }   
}       /* -----  end of function foo  ----- */
  
long int 
sfact ( long int x ) 
{
    long int result;
    sfact_helper(x, &result);
    return result;
}       /* -----  end of function sfact  ----- */
  
int
main ( int argc, char *argv[] )
{
    int sum = sfact(10);
    fprintf(stdout, "sum=%d\n", sum);
    return EXIT_SUCCESS;
}               /* ----------  end of function main  ---------- */

命令行中調(diào)用gcc,生成匯編語(yǔ)言:

1
Shell>: gcc –O1 –S –o test2.s test2.c

在函數(shù)sfact_helper中,用到了寄存器%rbx和%rbp,在覆蓋之前,GCC選擇了先保存他們的值,代碼6~9說(shuō)明該行為。在函數(shù)返回之前,GCC依次恢復(fù)了他們,就如代碼27-28展示的那樣。
看這段代碼你可能會(huì)困惑?為什么%rbx在函數(shù)進(jìn)入的時(shí)候,指向的是-16(%rsp),而在退出的時(shí)候,變成了32(%rsp) 。上文不是介紹過(guò)一個(gè)重要的特性嗎?訪問(wèn)棧幀之外的空間,這是GCC不用先分配空間再使用;而是先使用??臻g,然后在適當(dāng)?shù)臅r(shí)機(jī)分配。第11行代碼展示了空間分配,之后棧指針發(fā)生變化,所以同一個(gè)地址的引用偏移也相應(yīng)做出調(diào)整。

參數(shù)傳遞

X86時(shí)代,參數(shù)傳遞是通過(guò)入棧實(shí)現(xiàn)的,相對(duì)CPU來(lái)說(shuō),存儲(chǔ)器訪問(wèn)太慢;這樣函數(shù)調(diào)用的效率就不高,在x86-64時(shí)代,寄存器數(shù)量多了,GCC就可以利用多達(dá)6個(gè)寄存器來(lái)存儲(chǔ)參數(shù),多于6個(gè)的參數(shù),依然還是通過(guò)入棧實(shí)現(xiàn)。了解這些對(duì)我們寫(xiě)代碼很有幫助,起碼有兩點(diǎn)啟示:

  • 盡量使用6個(gè)以下的參數(shù)列表,不要讓GCC為難啊。
  • 傳遞大對(duì)象,盡量使用指針或者引用,鑒于寄存器只有64位,而且只能存儲(chǔ)整形數(shù)值,寄存器存不下大對(duì)象

讓我們具體看看參數(shù)是如何傳遞的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>    
#include <stdlib.h>
  
int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 )
{   
    int array[] = {100,200,300,400,500,600,700};
    int sum = array[arg1] + array[arg7];
    return sum;
}       /* -----  end of function foo  ----- */
  
    int 
main ( int argc, char *argv[] )
{
    int i = 1;
    int j = foo(0, 1, 2, 3, 4, 5, 6); 
    fprintf(stdout, "i=%d,j=%d\n", i, j); 
    return EXIT_SUCCESS;
}               /* ----------  end of function main  ---------- */

命令行中調(diào)用gcc,生成匯編語(yǔ)言:

1
Shell>: gcc –O1 –S –o test1.s test1.c

Main函數(shù)中,代碼31~37準(zhǔn)備函數(shù)foo的參數(shù),從參數(shù)7開(kāi)始,存儲(chǔ)在棧上,%rsp指向的位置;參數(shù)6存儲(chǔ)在寄存器%r9d;參數(shù)5存儲(chǔ)在寄存器%r8d;參數(shù)4對(duì)應(yīng)于%ecx;參數(shù)3對(duì)應(yīng)于%edx;參數(shù)2對(duì)應(yīng)于%esi;參數(shù)1對(duì)應(yīng)于%edi。
Foo函數(shù)中,代碼14-15,分別取出參數(shù)7和參數(shù)1,參與運(yùn)算。這里數(shù)組引用,用到了最經(jīng)典的尋址方式,-40(%rsp,%rdi,4)=%rsp + %rdi *4 + (-40);其中%rsp用作數(shù)組基地址;%rdi用作了數(shù)組的下標(biāo);數(shù)字4表示sizeof(int)=4。

結(jié)構(gòu)體傳參

應(yīng)@桂南要求,再加一節(jié),相信大家也很想知道結(jié)構(gòu)體是如何存儲(chǔ),如何引用的,如果作為參數(shù),會(huì)如何傳遞,如果作為返回值,又會(huì)如何返回。
看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
      
struct demo_s {
    char var8;
    int  var32;
    long var64;
};
  
struct demo_s foo (struct demo_s d)
{
    d.var8=8;
    d.var32=32;
    d.var64=64;
    return d;
}       /* -----  end of function foo  ----- */
  
    int 
main ( int argc, char *argv[] )
{
    struct demo_s d, result;
    result = foo (d);
    fprintf(stdout, "demo: %d, %d, %ld\n", result.var8, result.var32, result.var64); 
    return EXIT_SUCCESS;
}               /* ----------  end of function main  ---------- */

我們?nèi)笔【幾g選項(xiàng),加了優(yōu)化編譯的選項(xiàng)可以留給大家思考。

1
Shell>gcc  -S -o test.s test.c


上面的代碼加了一些注釋,方便大家理解,
問(wèn)題1:結(jié)構(gòu)體如何傳遞?它被分成了兩個(gè)部分,var8和var32合并成8個(gè)字節(jié)的大小,放在寄存器%rdi中,var64放在寄存器的%rsi中。也就是結(jié)構(gòu)體分解了。
問(wèn)題2:結(jié)構(gòu)體如何存儲(chǔ)? 注意看foo函數(shù)的第15~17行注意到,結(jié)構(gòu)體的引用變成了一個(gè)偏移量訪問(wèn)。這和數(shù)組很像,只不過(guò)他的元素大小可變。
問(wèn)題3:結(jié)構(gòu)體如何返回,原本%rax充當(dāng)了返回值的角色,現(xiàn)在添加了返回值2:%rdx。同樣,GCC用兩個(gè)寄存器來(lái)表示結(jié)構(gòu)體。
恩, 即使在缺省情況下,GCC依然是想盡辦法使用寄存器。隨著結(jié)構(gòu)變的越來(lái)越大,寄存器不夠用了,那就只能使用棧了。

總結(jié)

了解寄存器和棧幀的關(guān)系,對(duì)于gdb調(diào)試很有幫助;過(guò)些日子,一定找個(gè)合適的例子和大家分享一下。

參考

1. 深入理解計(jì)算機(jī)體系結(jié)構(gòu)
2. x86系列匯編語(yǔ)言程序設(shè)計(jì)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多

    日韩人妻一区二区欧美| 国产精品一区二区视频成人| 欧美丰满人妻少妇精品| 美女被啪的视频在线观看| 精品人妻少妇二区三区| 久久精品福利在线观看| 国产日本欧美特黄在线观看| 国产精品不卡高清在线观看| 日本免费一本一二区三区| 色欧美一区二区三区在线| 午夜国产精品国自产拍av| 日本一区二区三区黄色| 精品少妇人妻一区二区三区| 69精品一区二区蜜桃视频| 高清免费在线不卡视频| 色婷婷视频免费在线观看| 亚洲精品中文字幕熟女| 手机在线观看亚洲中文字幕| 国产麻豆一线二线三线| 日韩免费成人福利在线| 国产麻豆一线二线三线| 亚洲丁香婷婷久久一区| 中文字幕一区二区久久综合| 神马午夜福利免费视频| 日韩人妻有码一区二区| 国产韩国日本精品视频| 高清欧美大片免费在线观看| 欧美日韩乱一区二区三区| 亚洲国产欧美精品久久| 在线精品首页中文字幕亚洲 | 日韩专区欧美中文字幕| 日本道播放一区二区三区| 日本熟妇五十一区二区三区| 亚洲成人免费天堂诱惑| 国产一级一片内射视频在线| 99亚洲综合精品成人网色播| 欧美久久一区二区精品| 亚洲第一区二区三区女厕偷拍 | 欧美日韩精品视频在线| 成人午夜爽爽爽免费视频| 国产成人一区二区三区久久|