從彙編和底層的角度看c和類c語言
從彙編和底層的角度看c和類c語言
寫這篇文章的目的是對近期底層學習的總結,也算是勉勵自己吧,畢竟是光靠興趣苦逼自學不是自己專業的東西要承受很多壓力。
https://blog.csdn.net/jggyyhh/article/details/50429886?utm_source=blogxg
要想深入理解C語言就不得不要知道幾個知識點:
1.眾所周知用任意一高階語言(不是指令碼語言)寫的程式碼都要經過類似:預處理->編譯成彙編程式碼(compilation)->彙編(assembly)->連線(linking)這樣的階段。其中預處理產生.i檔案,compilation產生.s檔案,assembly產生.o檔案,最後連線才會產生可執行檔案,.o檔案中不同機器上是不同的,而Java的能夠“一次編譯,到處執行“是因為Java不會像c那樣在不同機器上產生不同的.o檔案,而是用jvm虛擬機器遮蔽了不同機器上的不同之處,於是只有不同的機子上都要Java的外掛,一次編譯後的檔案就能到處執行。(可以想象的是為啥Android機的硬體配置往往要比iphone好,因為Android機正用了java的技術故中間多了一次轉換過程當然效率要比用object-c編的iOS程式低,不過據說最近jvm採用了一些技術將效率提高了不小,不過這個我還沒研究過就不說了)。
2.當你的程式碼被編譯器編譯成可執行檔案(不一定是exe,這是個誤區,以PE檔案為例,這些格式其實是在PE檔案頭偏移量為0016h處的Characteristics欄位表明的,如果是exe這一欄位為0x0f01),不同的作業系統下的可執行檔案是不同的,linux下為ELF,windows下為PE。由於我比較熟悉PE檔案的格式,我就拿PE檔案做個例子,你反彙編任意一個Windows的可執行檔案你就會發現每個檔案都被分成了很多個塊,大致分成了.text,.idata,.rdata,.data,.rsrc塊,這是為啥呢?這其實是為了方便程式對映到程序記憶體空間,因為為了方便管理和實現各種機制,程序的記憶體空間是分段的,在linux下一個程序的記憶體空間大致是這樣:
其中程序的使用者態的線性地址空間是從0x00000000到0xbfffffff,也就是一般的應用程式跑的線性地址空間(記憶體中每一個位元組的資料被賦予一個地址),注意這裡是線性地址空間, 你反彙編左側的地址空間是邏輯地址,
如上圖左側的是邏輯地址,(這些地址都是16進位制),邏輯地址要經過分段機制才能指向線性地址,而線性地址要經過分頁才能指向實體地址(實體地址才是記憶體條上),(有些作業系統沒有分段機制,邏輯地址等於線性地址)。這其中的細節展開是一章的內容,我就不多說,有興趣的可以看下linux核心方面的書籍,你要清楚你的程式要跑起來必定cpu要為你的程式分配記憶體(其實還有很多東西),跑起來後看情況你的程式會以一個程序或者執行緒的狀態出現在作業系統上(程序的描述可不是簡單的pid就能標識的,而是task_struct這個被稱為程序描述符的東西同樣這些東西要參考核心方面的書籍)。下圖是windows可執行檔案的對映(比較懶啊,直接把筆記弄上去了):
我想經過我這樣一番描述你大致模糊的清楚一個程式在你電腦的存在和執行是啥情況了,下面我將分析語句了
靜態作用域:
沒錯,這就是《編譯原理》裡的那部分內容,不過我加上了我的從底層上的一些見解即解釋,什麼是靜態作用域呢?通俗的說就是你通過原始碼就能判斷一個宣告的作用範圍,在
這個範圍內所有對該宣告變數的使用都指向那個宣告。c語言的(類c語言)作用域規則是基於程式結構的(塊),也就是和你的“{ }”符號的使用有關,如下圖:
最後一個cout<<a<<b列印的a的值為1,因為在一個塊(也是一個作用域)內的語句會首先使用該塊內的宣告,如B3域內cout<<a<<b列印的a的值是3,如果該塊內沒有這個宣告,則
找到其父塊,如B3域內cout<<a<<b列印的b的值是2。其實,int a是一種宣告,int a=1也是宣告,而定義是在你宣告過後類似於 a=1這種語句,其實定義可以看成是定值,而a這個東西
只是一個名字,名字和變數(記憶體位置也即不同的記憶體地址)的關係如圖:
在不同域名字可以一樣,但是因為其環境(作用域)不同其實它指向的記憶體位置是不同的,且你在定義之前必須宣告,要不然它不清楚是對哪個記憶體位置進行賦值操作,如:B2域和B3域都有個int a =*的宣告,其實它們分別指向不同的記憶體位置所以可以存不同的值。故定義(定值)所指向的變數(實際上是記憶體位置)是取決於作用域的宣告的,即使是相同語句(名字)也會隨環境變化而賦予不同變數值。又因為C語言在作用域內是按順序執行語句的,也就有了這個例子網上找的例子,其中int max(int,int);聲明瞭個函式變數(有了一個相應的記憶體地址),它的作用域為整個程式,但是這個變數在這個作用域內還沒有值,int main()函式下是另一個作用域,在這個作用域內並沒有max函式的宣告故它呼叫其父作用域的宣告,在其父作用域內有個函式宣告(因為在執行main函式前就執行了int max(int,int);),故函式成功呼叫,此例中你將int max(int,int);放在main()函式之下就不行了,這是因為C語言是順序執行的,其實此例的最後7行既可以說是定義也可以說是宣告,就像int a=1一樣。
而java,c++與c語言的不同在於,它多了public,private,protected等等這些限制作用域的關鍵字,而不像c語言那樣僅僅靠“{ }”程式設計師自己限制作用域範圍或者函式(不同函式也是個不同作用域),與是乎java就有了許多個不同型別的被封裝的作用域,比如說public宣告的方法能被所有定義的類的物件呼叫。。。於是乎產生了物件這種東西,我認為c語言不是面向物件的語言的根本原因是它沒有對作用域進行自定義化的封裝,沒有產生有獨特性質方法(在c語言是函式)的“物件”,通俗上說是沒有類似public這樣的關鍵字。(只是個人見解,大牛見了不要見笑)
接下是重點了: 上面說了那麼多其實都沒深入到彙編層次也沒用上之前我敘述的程序記憶體的知識,也沒有從底層給出不同作用域的實現機制,接下來才是關鍵之處。
這是我簡化後的linux程序記憶體儲存方式(類似於第一張圖,其實第一張圖也是簡略後的,.text段和.data段中有很多其它的東西(segement),畢竟你一個比較大的程式要有動態連結庫還有一些則與linux中ANSI C的函式庫libc的函式有關,這些東西要不涉及核心呼叫要不和庫函式有關有的甚至與gcc編譯器有關),線性地址從左到右依次變大,其中.text段中存有隻讀的二進位制檔案, .data 存有全域性初始化變數如:static Int a=0 。.bss段存了全域性未初始化變數如:static Int a。你會納悶那我函式記憶體儲的變數 如在B2中的int b=2,b所指向的變數存在哪呢?其實它們都存在stack這裡面,stack也就是棧的意思,只要沒有全域性化宣告的變數都存在stack裡面。宣告在static內的變數是固定的(地址固定),也即一旦你在程式裡面改變了這個名字的值那麼它會在你程式執行週期內永久改變,無論你改變的語句所在的作用域是啥。接下來我將通過反彙編一些程式為你揭示那些普通函式的變數是怎樣在棧記憶體在的:(沒學過彙編的朋友接下來的內容你可能會看不懂,但是沒法,我的主題是深入理解c語言,不過這之上的內容我認為也是很有意義的)
首先在linux用 objdump -S test1.o 命令反彙編我之前編好的test.o的還未連結成可執行檔案的.o檔案(可用 gcc -c -o test1.c 命令產生,-S是將彙編程式碼和從語言同時顯示),因為.o檔案不是可執行檔案沒連結,故當在一個函式內呼叫另一個函式時沒有call語句,.o檔案連結後會產生很多不是原始碼的段,這是系統自帶的呼叫或者庫連結甚至有些段是用來傳遞使用者態程序的暫存器資料給核心的,這些機制的存在我認為不僅僅出於系統功能的作用,還有很大部分出於安全性,最早之前的棧分配方式是非常容易被緩衝區攻擊的。之前的C語言程式反彙編的程式碼大致是這樣的點此處檢視,棧的保護方式多種多樣,有的在返回地址處加墊片,有的用ASLR技術也就是所謂的地址空間隨機化技術,在我分析完程式碼後會簡單地演示這種技術,這些技術隨著linux不斷髮展而更新,使得作業系統越來越安全,這就是開源的魅力,相當於全世界的高手都在參與作業系統的更新,這也是linux的魅力與活力。我接觸過shellcode的編寫,雖然依舊很菜,但是我還是大致知道常見的幾種緩衝區攻擊和一些過時的漏洞。好了,言歸正傳,由於我對現在版本核心的保護機制不瞭解,故有些語句作用我不太清楚,只能瞎猜測一番,如有大牛看到不要見笑,前面說到.o檔案,我認為.o檔案不太適合演示,故我演示的是反彙編可執行檔案test1,用 gcc -o test test1.c命令編譯,然後用objdump -d -M i386 test1 反彙編,這裡的-M命令選項是指定組合語言的格式,用objdump -i 可以看到格式選項,從語句形式的角度看一共有兩種格式,intel和AT&T,預設的是AT&T,這兩種格式差別不大,然後每種格式下分了32位和64位兩種,其實是暫存器改變了,不過64位暫存器是相容32位的,為了方便我將統一使用AT&T的32位(i386)指令集,你甚至可以加兩個-M選項如objdump -d -M i386 -M intel test1 這使用的是 intel的32位指令集。64位暫存器和32位的相容如下圖:
原始碼test1.c:
#include<stdio.h>
int sum(int temp1,int temp2);
int main()
{
int i;
i=sum(2,3);
return 0;
}
int sum(int temp1,int temp2)
{ int c=temp1;
int b=temp2;
int a= b+c;
return a;
}
在這我要糾正一個很多人都會犯的錯誤,就是寫void main()這種形式的主函式,main()函式的返回值必須是int,linux程序退出分為正常退出和異常退出兩種,正常退出中有一種是在main()函式裡執行return操作(其它的是呼叫核心函式exit()和_exit(),其中exit()會將記憶體緩衝區資料回寫給檔案)main函式的返回值由 __libc_start_main接收,並傳遞給exit,return+非零值 表示非正常退出(另外程序中斷時會呼叫about()函式表示非正常退出),其實c語言的return機制非常像Java中的try(),catch()的異常丟擲,然而我們大多數人把return當做返回值用,其實從另一角度這也可以看做是“異常”處理吧,如果在main()函式(或者其它函式)裡呼叫其它函式,當被呼叫函式內的return執行完後會將控制權(看後面就知道是cpu指令暫存器eip(rip))交給呼叫函式,如果main()函式中執行完return則將控制權交給作業系統,在早期的編譯器版本中void main()會報錯,新版本的編譯器會在void main()中自動加入return 0。這個錯誤看似沒啥,但是搞不好會被技藝高超的黑客所利用。
好迴歸正題:反彙編程式碼:我主要關心原始碼的函式main和sum,因為其它段是和原始碼內容基本無關的(其實是有些東西太複雜不懂)
test1: file format elf64-x86-64
Disassembly of section .init:
0000000000400370 <_init>: /這個區和最後的_fini和gcc編譯器在連結時載入一般init與核心呼叫有關,這個我們
不太關心(其實我也沒深入研究過<img alt="害羞" src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/shy.gif" />不懂,大概有個模糊概念)
400370: 48 dec %eax
400371: 83 ec 08 sub $0x8,%esp
400374: 48 dec %eax
400375: 8b 05 45 05 20 00 mov 0x200545,%eax
40037b: 48 dec %eax
40037c: 85 c0 test %eax,%eax
40037e: 74 05 je 400385 <_init+0x15>
400380: e8 2b 00 00 00 call 4003b0 <[email protected]>
400385: 48 dec %eax
400386: 83 c4 08 add $0x8,%esp
400389: c3 ret
Disassembly of section .plt:
0000000000400390 <[email protected]>:
400390: ff 35 3a 05 20 00 pushl 0x20053a
400396: ff 25 3c 05 20 00 jmp *0x20053c
40039c: 0f 1f 40 00 nopl 0x0(%eax)
00000000004003a0 <[email protected]>:
4003a0: ff 25 3a 05 20 00 jmp *0x20053a
4003a6: 68 00 00 00 00 push $0x0
4003ab: e9 e0 ff ff ff jmp 400390 <_init+0x20>
00000000004003b0 <[email protected]>:
4003b0: ff 25 32 05 20 00 jmp *0x200532
4003b6: 68 01 00 00 00 push $0x1
4003bb: e9 d0 ff ff ff jmp 400390 <_init+0x20>
Disassembly of section .text:
00000000004003c0 <_start>: /這是真正的程式入口處
4003c0: 31 ed xor %ebp,%ebp
4003c2: 49 dec %ecx
4003c3: 89 d1 mov %edx,%ecx
4003c5: 5e pop %esi
4003c6: 48 dec %eax
4003c7: 89 e2 mov %esp,%edx
4003c9: 48 dec %eax
4003ca: 83 e4 f0 and $0xfffffff0,%esp/看到這個語句我想到了墊片保護棧的技術
但是好像有點不太一樣,這處語句附近一定儲存了argc 和argv[].
4003cd: 50 push %eax
4003ce: 54 push %esp
4003cf: 49 dec %ecx
4003d0: c7 c0 70 05 40 00 mov $0x400570,%eax
4003d6: 48 dec %eax
4003d7: c7 c1 00 05 40 00 mov $0x400500,%ecx
4003dd: 48 dec %eax
4003de: c7 c7 b6 04 40 00 mov $0x4004b6,%edi
4003e4: e8 b7 ff ff ff call 4003a0 <[email protected]>
4003e9: f4 hlt
4003ea: 66 0f 1f 44 00 00 nopw 0x0(%eax,%eax,1)
00000000004003f0 <deregister_tm_clones>:
4003f0: b8 07 09 60 00 mov $0x600907,%eax
4003f5: 55 push %ebp
4003f6: 48 dec %eax
4003f7: 2d 00 09 60 00 sub $0x600900,%eax
4003fc: 48 dec %eax
4003fd: 83 f8 0e cmp $0xe,%eax
400400: 48 dec %eax
400401: 89 e5 mov %esp,%ebp
400403: 76 1b jbe 400420 <deregister_tm_clones+0x30>
400405: b8 00 00 00 00 mov $0x0,%eax
40040a: 48 dec %eax
40040b: 85 c0 test %eax,%eax
40040d: 74 11 je 400420 <deregister_tm_clones+0x30>
40040f: 5d pop %ebp
400410: bf 00 09 60 00 mov $0x600900,%edi
400415: ff e0 jmp *%eax
400417: 66 0f 1f 84 00 00 00 nopw 0x0(%eax,%eax,1)
40041e: 00 00
400420: 5d pop %ebp
400421: c3 ret
400422: 66 66 66 66 66 2e 0f data16 data16 data16 data16 nopw %cs:0x0(%eax,%eax,1)
400429: 1f 84 00 00 00 00 00
0000000000400430 <register_tm_clones>: /從字面上看這與將暫存器複製到核心有關
400430: be 00 09 60 00 mov $0x600900,%esi
400435: 55 push %ebp
400436: 48 dec %eax
400437: 81 ee 00 09 60 00 sub $0x600900,%esi
40043d: 48 dec %eax
40043e: c1 fe 03 sar $0x3,%esi
400441: 48 dec %eax
400442: 89 e5 mov %esp,%ebp
400444: 48 dec %eax
400445: 89 f0 mov %esi,%eax
400447: 48 dec %eax
400448: c1 e8 3f shr $0x3f,%eax
40044b: 48 dec %eax
40044c: 01 c6 add %eax,%esi
40044e: 48 dec %eax
40044f: d1 fe sar %esi
400451: 74 15 je 400468 <register_tm_clones+0x38>
400453: b8 00 00 00 00 mov $0x0,%eax
400458: 48 dec %eax
400459: 85 c0 test %eax,%eax
40045b: 74 0b je 400468 <register_tm_clones+0x38>
40045d: 5d pop %ebp
40045e: bf 00 09 60 00 mov $0x600900,%edi
400463: ff e0 jmp *%eax
400465: 0f 1f 00 nopl (%eax)
400468: 5d pop %ebp
400469: c3 ret
40046a: 66 0f 1f 44 00 00 nopw 0x0(%eax,%eax,1)
0000000000400470 <__do_global_dtors_aux>:
400470: 80 3d 89 04 20 00 00 cmpb $0x0,0x200489
400477: 75 11 jne 40048a <__do_global_dtors_aux+0x1a>
400479: 55 push %ebp
40047a: 48 dec %eax
40047b: 89 e5 mov %esp,%ebp
40047d: e8 6e ff ff ff call 4003f0 <deregister_tm_clones>
400482: 5d pop %ebp
400483: c6 05 76 04 20 00 01 movb $0x1,0x200476
40048a: f3 c3 repz ret
40048c: 0f 1f 40 00 nopl 0x0(%eax)
0000000000400490 <frame_dummy>:
400490: bf e8 06 60 00 mov $0x6006e8,%edi
400495: 48 dec %eax
400496: 83 3f 00 cmpl $0x0,(%edi)
400499: 75 05 jne 4004a0 <frame_dummy+0x10>
40049b: eb 93 jmp 400430 <register_tm_clones>
40049d: 0f 1f 00 nopl (%eax)
4004a0: b8 00 00 00 00 mov $0x0,%eax
4004a5: 48 dec %eax
4004a6: 85 c0 test %eax,%eax
4004a8: 74 f1 je 40049b <frame_dummy+0xb>
4004aa: 55 push %ebp
4004ab: 48 dec %eax
4004ac: 89 e5 mov %esp,%ebp
4004ae: ff d0 call *%eax
4004b0: 5d pop %ebp
4004b1: e9 7a ff ff ff jmp 400430 <register_tm_clones>
00000000004004b6 <main>:
4004b6: 55 push %ebp //儲存原棧底,然後棧頂指標(esp)向上移動4位,因為
ebp是32位暫存器(32bit)即4個位元組(8bit),記憶體給每個位元組分配一個地址。
4004b7: 48 dec %eax //這句的eax自減1,和下下句我完全不知道是啥意思,我
猜測是某種計數用的。
4004b8: 89 e5 mov %esp,%ebp//建立新棧底
4004ba: 48 dec %eax
4004bb: 83 ec 10 sub $0x10,%esp//esp向上偏移16位,因為$0x10是16進位制,開闢了個
能存4個整型(int)資料的區域或者16個char資料型別的區域
4004be: be 03 00 00 00 mov $0x3,%esi//將第二個引數值3存入esi
4004c3: bf 02 00 00 00 mov $0x2,%edi//將第一個引數值2存入esi
4004c8: e8 0a 00 00 00 call 4004d7 <sum>/將jmp 4004d7即跳到sum函式作用域,然後將下一
條語句的地址作為返回地址壓棧
4004cd: 89 45 fc mov %eax,-0x4(%ebp)//將從sum那計算出的a值存到ebp的上面第一個
整型區域(偏移量4).這也就是i的記憶體空間
4004d0: b8 00 00 00 00 mov $0x0,%eax//清空eax
4004d5: c9 leave //相當於 mov %esp,%ebp pop %ebp兩條語句,目的是還原原棧底
4004d6: c3 ret //相當於 pop eip,作用是返回呼叫該函式的函式的空間,作用
域被改變
00000000004004d7 <sum>:
4004d7: 55 push %ebp//儲存原棧底,然後棧頂指標(esp)向上移動4位,因為
ebp是32位暫存器(32bit)即4個位元組(8bit),記憶體給每個位元組分配一個地址。
4004d8: 48 dec %eax
4004d9: 89 e5 mov %esp,%ebp//同main
4004db: 89 7d ec mov %edi,-0x14(%ebp)//將第一個引數值2從esi存入ebp上方偏移量為
14的記憶體空間處
4004de: 89 75 e8 mov %esi,-0x18(%ebp)//將第2個引數值3從esi存入ebp上方偏移量為
14的記憶體空間處
4004e1: 8b 45 ec mov -0x14(%ebp),%eax//將第一個引數值2從存入eax
4004e4: 89 45 fc mov %eax,-0x4(%ebp)//將eax的值2從存入ebp上方偏移量為
4的記憶體空間處,相當於sum函式內的c指向的空間位置
4004e7: 8b 45 e8 mov -0x18(%ebp),%eax//將第二個引數值3從存入eax
4004ea: 89 45 f8 mov %eax,-0x8(%ebp)//將eax的值3從存入ebp上方偏移量為
8的記憶體空間處,相當於sum函式內的b指向的空間位置
4004ed: 8b 55 f8 mov -0x8(%ebp),%edx
4004f0: 8b 45 fc mov -0x4(%ebp),%eax
4004f3: 01 d0 add %edx,%eax//這上面3句相當於函式內的a=b+c,且a的值存入eax中
4004f5: 89 45 f4 mov %eax,-0xc(%ebp)//將eax的值a(5)從存入ebp上方偏移量為
12的記憶體空間處,相當於sum函式內的a指向的空間位置
4004f8: 8b 45 f4 mov -0xc(%ebp),%eax//將a的值存入eax,為之後main函式傳值做鋪墊
4004fb: 5d pop %ebp//恢復原棧底
4004fc: c3 ret /同main
4004fd: 0f 1f 00 nopl (%eax) //佔位用,無實際意義
0000000000400500 <__libc_csu_init>:
400500: 41 inc %ecx
400501: 57 push %edi
400502: 41 inc %ecx
400503: 89 ff mov %edi,%edi
400505: 41 inc %ecx
400506: 56 push %esi
400507: 49 dec %ecx
400508: 89 f6 mov %esi,%esi
40050a: 41 inc %ecx
40050b: 55 push %ebp
40050c: 49 dec %ecx
40050d: 89 d5 mov %edx,%ebp
40050f: 41 inc %ecx
400510: 54 push %esp
400511: 4c dec %esp
400512: 8d 25 c0 01 20 00 lea 0x2001c0,%esp
400518: 55 push %ebp
400519: 48 dec %eax
40051a: 8d 2d c0 01 20 00 lea 0x2001c0,%ebp
400520: 53 push %ebx
400521: 4c dec %esp
400522: 29 e5 sub %esp,%ebp
400524: 31 db xor %ebx,%ebx
400526: 48 dec %eax
400527: c1 fd 03 sar $0x3,%ebp
40052a: 48 dec %eax
40052b: 83 ec 08 sub $0x8,%esp
40052e: e8 3d fe ff ff call 400370 <_init>
400533: 48 dec %eax
400534: 85 ed test %ebp,%ebp
400536: 74 1e je 400556 <__libc_csu_init+0x56>
400538: 0f 1f 84 00 00 00 00 nopl 0x0(%eax,%eax,1)
40053f: 00
400540: 4c dec %esp
400541: 89 ea mov %ebp,%edx
400543: 4c dec %esp
400544: 89 f6 mov %esi,%esi
400546: 44 inc %esp
400547: 89 ff mov %edi,%edi
400549: 41 inc %ecx
40054a: ff 14 dc call *(%esp,%ebx,8)
40054d: 48 dec %eax
40054e: 83 c3 01 add $0x1,%ebx
400551: 48 dec %eax
400552: 39 eb cmp %ebp,%ebx
400554: 75 ea jne 400540 <__libc_csu_init+0x40>
400556: 48 dec %eax
400557: 83 c4 08 add $0x8,%esp
40055a: 5b pop %ebx
40055b: 5d pop %ebp
40055c: 41 inc %ecx
40055d: 5c pop %esp
40055e: 41 inc %ecx
40055f: 5d pop %ebp
400560: 41 inc %ecx
400561: 5e pop %esi
400562: 41 inc %ecx
400563: 5f pop %edi
400564: c3 ret
400565: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%eax,%eax,1)
40056c: 00 00 00 00
0000000000400570 <__libc_csu_fini>:
400570: f3 c3 repz ret
Disassembly of section .fini:
0000000000400574 <_fini>:
400574: 48 dec %eax
400575: 83 ec 08 sub $0x8,%esp
400578: 48 dec %eax
400579: 83 c4 08 add $0x8,%esp
40057c: c3 ret
從原始碼的分析可知,每一個函式的作用域相當於一個建立的棧,其內部的變數存入各自的棧中,呼叫一個函式只是將eip即cpu指令暫存器的值指向被呼叫函式的記憶體空間的開頭然後將該呼叫語句的下一句地址壓棧,且呼叫函式的棧頂是被呼叫函式的棧底,圖類似於我上面給的超連結處的內容的圖,只不過現在的核心為了安全捨棄了將引數壓棧的這種呼叫方式而是通過暫存器esi,edi傳遞,想想也是,這樣做防止了以前用類似strcpy()函式的缺陷而造成的緩衝區溢位。
下面我將演示ASLR技術也就是所謂的地址空間隨機化技術,這個技術能干擾黑客編寫shellcode,因為很多shellcode的編寫要準確計算出esp到ebp之間的長度,然後用惡意程式碼填充,這我就不說方法了,找一本國外的黑客書籍都有介紹,但是基本上是沒有的,就如我之前所說現在的linux核心版本加入了很多保護機制,你必須要繞開它你的shellcode才有用。
回到正題:這是我編的一段小程式碼,作用是列印esp暫存器的內容即棧頂地址
test.c原始碼:
#include<stdio.h>
unsigned int get_esp(){
__asm__("movl %esp, %eax");
}
int main(){
printf("STACK ESP:0x%x\n", get_esp());
}
執行結果如圖:
你會發現每執行一次,esp的值都有變化。
使用命令 echo "0" > /proc/sys/kernel/randomize_va_space #on slackware systems
將這個保護選項關閉後再執行,結果如下:
現在棧頂地址固定了,一般我研究shellcode時會把棧頂值固定,這樣才容易出效果,畢竟比較菜。