第五週(10.05-10.11):
學習計時:共7小時
讀書:3h
代碼:1h
做業:1h
博客:2hhtml
1.Intel處理器的模型數組
8086
80286
i386
i486
Pentium
PentiumPro
Pentium II
Pentium III
Pentium 4
Pentium 4E
Core 2
Core i7
最初的8086提供的存儲器模型和擴展已通過時了,做爲替代,Linux使用了平坦存儲方式。安全
假設一個C程序,有兩個文件p1.c和p2.c。咱們在一臺IA32機器上,用Unix命令行編譯這些代碼以下:
unix> gcc -O1 -o p p1.c p2.c
實際上gcc命令調用了一系列程序,將源代碼轉化成可執行代碼。首先,C預處理器擴展源代碼,插入全部用#include命令指定的文件,並擴展全部用#define聲明指定的宏。而後,編譯器產生兩個源代碼的彙編代碼,名字分別爲p1.s和p2.s。接下來,彙編器將彙編代碼轉化成二進制目標代碼文件,名爲p1.o和p2.o。目標代碼是機器代碼的一種形式,它包括全部指令的二進制表示,可是尚未填入地址的全局至。最後,連接器將兩個目標代碼文件與實現庫函數(例如printf)的代碼合併,併產生最終的可執行代碼文件p。可執行代碼是咱們要考慮的機器代碼的第二種形式,也就是處理器執行的代碼格式。數據結構
1.兩種抽象:指令集體系結構;機器級程序使用的存儲器地址是虛擬地址,提供的存儲器模型看上去是一個很是大的字節數組。
2.程序存儲器(program memory)包含:程序的可執行機器代碼、操做系統須要的一些信息、棧、堆。程序存儲器用虛擬地址來尋址(此虛擬地址不是機器級虛擬地址)。操做系統負責管理虛擬地址空間(程序級虛擬地址),將虛擬地址翻譯成實際處理器存儲器中的物理地址(機器級虛擬地址)。less
在命令行上使用-S選項獲得C語言編譯器產生的彙編代碼函數
ATT與Intel彙編代碼格式工具
因爲是從16位體系結構擴展成32位,intel用術語字(word)表示16位數據類型,所以32位爲雙字(double words),64位數爲4字(quad words)。
如下是比較容易模糊的數據類型大小:性能
32位機上:float 4 long int 4 double 8 longlong 8 char* 4 unsigned long 4 64位機上:float 4 long int 8 double 8 longlong 8 char* 8 unsigned long 8
另外,GCC 用long double表示擴展精度(10字節),出於存儲器性能考慮,會被存儲爲12字節學習
大多數指令有一到多個操做數,操做數有三種:測試
當即數:即常數值 寄存器:表示某個寄存器內容 存儲器引用:根據計算出來的地址(一般稱有效地址)訪問某個存儲器位置
所以尋址方式也有多種,如:當即數尋址、寄存器尋址、絕對尋址、間接尋址、變址尋址、伸縮化 的變址尋址
1.經過書中圖3.5可知,當push一個數值時,棧指針減少,向下移動;當pop一個數據時棧指針向上移動。通常用 %esp來存儲棧指針的地址。
2.move指令:將源操做符的值複製到目的操做數中。源操做數指定的值是一個當即數,存儲在寄存器中或者存儲器中。目的操做數指定一個位置,要麼是一個寄存器,要麼是一個存儲器地址。。傳送指令的鏈各個操做數不能都指向存儲器位置。這些指令的寄存器操做數,對於movl來講,能夠是8個32位寄存器(%eax~%ebp),對於movw來講,能夠是8個16位寄存器(%ax~%bp),movb可使單字節寄存器元素(%ah~%bh,%al~%bl)。
3.IA32加了一條限制,傳送指令的兩個操做數不能都指向存儲器位置。講一個值從一個存儲器位置複製到另外一個存儲器位置須要兩條指令-----第一條指令將源值加載到寄存器中,第二條將該寄存器值寫入目的位置。
下面經過一個例子說明C語言中指針使用的方式,函數exchange代碼以下:
int exchange(int *xp, int y) { int x = *xp; *xp = y; return x; } int a = 4; int b = exchange(&a, 3); printf("a = %d, b = %d\n", a, b);
這段代碼會打印出: a = 3, b = 4
關鍵部分的彙編代碼以下:
xp地址的值存儲在8(%ebp), y的值存儲在12(%ebp)
movl 8(%ebp) %edx #獲取xp地址的值,存儲在%edx movl (%edx), %eax #獲取xp地址所指向的值賦予變量x,函數結束時返回這個值 movl 12(%ebp), %ecx #獲取y的值,存儲在%ecx movl %ecx, (%edx) #%ecx的值存儲在%edx所指向的值, 這時候*xp的值爲y,xp地址的值沒有變化
局部變量好比x,一般時保存在寄存器中。
加載有效地址(load effective address)指令leal其實是movl指令的變形。經過下面一個例子來講明它的含義:
假設%edx的值爲x
movl 7(%edx, %edx, 4), %eax #計算7 + x + 4x = 5x +7
那麼%eax的值就是地址5x+7地址處所存儲的值.
leal 7(%edx, %edx, 4), %eax #計算7 + x + 4x = 5x +7
那麼%eax的值就是地址的值5x+7,而不是這個地址存儲的值.
若是一個操做只有一個操做數,既是源又是目的,這個操做就是一元操做。 若是一個操做有兩個操做數,第二個操做數既是源又是目的,這個操做就是二元操做。 以下:
指令 效果 操做
INC D D <- D + 1 一元操做
ADD S, D D <- D + S 二元操做
類型 操做 命令
非循環移位 邏輯左移/算術左移 SHL/SAL
非循環移位 邏輯右移 SHR
非循環移位 算術右移 SAR
循環移位 不含進位位的循環左移 ROL
循環移位 不含進位位的循環右移 ROR
循環移位 含進位位的循環左移 RCL
循環移位 含進位位的循環右移 RCR
除了整數寄存器,CPU還維護着一組單個位的條件碼(codition code)寄存器,最經常使用的條件碼有:
CF:進位標誌。最近的操做使最高位產生了進位。能夠用來檢測無符號操做數的溢出。
ZF:零標誌。最近的操做得出的結果爲0。
SF:符號標誌。最近的操做獲得的結果爲負。
OF:溢出標誌。最近的操做致使一個補碼溢出——正溢出或負溢出。
條件碼一般不會直接讀取經常使用的使用方法有三種:
能夠根據條件碼的某個組合,將一個字節設置爲0或者1;
能夠條件跳轉到程序的某個其它部分;
能夠有條件地傳送數據.
對於第一種狀況,舉例說明一下:
指令 同義名 效果 設置條件
sete D setz D<-ZF 相等/零
setne D setnz D<-~ZF 不等/非零
sets D D<-SF 負數
setns D D<-~SF 非負數
setg D setnle D<-~(SF ^ DF)&~ZF 大於(有符號>)
SET指令。每條指令根據條件碼的某個組合將一個字節設置爲0或者1,而不是直接訪問條件碼寄存器。
1.正常狀況下,指令按照它們出現的順序一條一條地執行。跳轉(jump)指令會致使執行切換到程序中一個全新的位置。在彙編代碼中,這些跳轉的目的地一般用一個標號(label)指明。當執行與PC(程序計數器)相關的尋址時,程序計數器的值是跳轉指令後面的那條指令的地址,而不是跳轉指令自己的地址。
2.Jmp指令也有不少兄弟,其兄弟形式如set,例如:je jne ……
如何將條件表達式和語句從C語言翻譯成機器代碼,最經常使用的方式是結合有條件和無條件跳轉。
循環和條件分支所利用的都是jump指令和標記寄存器的值
實現條件操做的傳統方法是利用控制的條件轉移。可是在現代處理器上,它可能會很是的低效率。數據的條件轉移是一種替代的策略。這種方法先計算一個條件操做的兩種結果,而後再根據條件是否知足選取一個。
switch(開關)語句能夠根據一個整數索引值進行多重分支(multi-way branching)。處理具備多種可能結果的測試時。這種語句特別有用。它們不只提升了C代碼的可讀性,並且經過使用跳轉表(jump table)這種數據結構使得實現更加高效。
一個過程調用包括將數據(以過程參數和返回值的形式)和控制從代碼的一部分傳遞到另外一部分。另外,它還必須在進入時爲過程的局部變量分配空間,並在退出時釋放這些空間。數據傳遞、局部變量的分配和釋放經過操縱程序棧來實現。
爲單個過程分配的那部分棧稱爲棧幀(stack frame)。寄存器%ebp爲幀指針,而寄存器%esp爲棧指針。棧幀結構(棧用來傳遞參數、存儲返回信息、保存寄存器,以及本地存儲)
支持過程調用和返回的指令:
指令 描述
call Label 過程調用
call *Operand 過程調用
leave 爲返回準備棧
ret 從過程調用中返回
call指令的效果是將返回地址入棧,並跳轉到被調用過程的起始處。返回地址是在程序中緊跟在call後面的那條指令地址。
程序寄存器組是惟一能被全部過程共享的資源。雖然在給定時刻只能有一個過程是活動的,可是咱們必須保證當一個過程(調用者)調用另外一個過程(被調用者)時,被調用者不會覆蓋某個調用者稍後會使用的寄存器的值。根據慣例,寄存器%eax、%edx和%ecx被劃分爲調用者保存寄存器。當過程P調用Q時,Q能夠覆蓋這些寄存器,而不會破任何P所須要的數據。另外一方面,寄存器%ebx、%esi和%edi被劃分爲被調用者保存寄存器。這意味着Q必須在覆蓋這些寄存器以前,先把它們保存到棧中,並在返回前恢復它們。
每一個調用過程都有它本身的私有空間,多個未完成調用的局部變量不會相互影響。
對於數據類型T和整型常數N,聲明以下:
T A[N] 它有兩個效果:
它在存儲器中分配一個( L \bullet N )字節的連續區域;這裏L是數據類型T的大小(單位爲字節), 用 ( x_{A} ) 來表示起始位置。
它引入了標符A;能夠用A做爲指向數組開頭的指針,這個指針的值就是 (x_{A}) 。數組元素i被存放在地址爲 (x_{A} + L \bullet i)的地方。
c99引入了一種能力,容許數組的維度是表達式,在數組分配的時候才計算出來。
C語言提供了兩種結合不一樣類型的對象來建立數據類型的機制:結構(structure),用關鍵字struct聲明, 將多個對象集合到一個單位中;聯合(union), 用關鍵字union聲明,容許用集中不一樣的類型來引用一個對象。
當用聯合將各類不一樣大小的數據類型結合到一塊兒時,字節順序問題就變得很重要了。這時,大端和小端的區別就會體現。
許多計算機系統對基本數據類型合法地址作出了一些限制,要求某種類型對象的地址必須是某個值k(一般是2,4或8)的倍數。這種對齊限制簡化了造成處理器和存儲器系統之間接口的硬件設計
指針和它們映射到機器代碼的關鍵原則:
每一個指針都對應一個類型。void * 類型表明通用指針。
每一個指針都有一個值。這個值是某個指定類型對象的地址。
指針用&運算符建立。經常使用leal指令來計算表達式的值。int * p = &x
操做符用於指針的間接引用。
數組與指針緊密聯繫。
將指針從一種類型強制轉換爲另外一種類型,只改變它的類型,而不改變它的值。
指針也能夠指向函數。
1.C對於數組引用不進行任何邊界檢查,並且局部變量和狀態信息(例如保存的寄存器值和返回地址),都存放在棧中。這兩種狀況結合到一塊兒就可能致使嚴重的程序錯誤,對越界的數組元素的寫操做會破壞存儲在棧中的狀態信息。一種常見的狀態破壞稱爲緩衝區溢出(buffer overflow)。
2.蠕蟲(worm)是這樣一個程序,它能夠本身運行,而且可以將一個徹底有效的本身傳播到其餘機器。病毒(virus)是這樣一段代碼,它能將本身添加到包括操做系統在內的其餘程序中,但它不能獨立運行。
3.對越界的數組元素進行寫操做會破壞存儲在棧中的狀態信息。
4.緩衝區溢出:在棧中分配某個字節數組來保存一個字符串,可是字符串的長度超出了爲數組分配的空間。
5.緩衝區溢出的一個更加致命的使用就是讓程序執行它原本不肯意執行的函數——一般給程序一個字符串,字符串包含兩部分:可執行代碼的字節編碼(攻擊代碼),以及,會用一個指向前面可執行代碼的指針覆蓋返回地址。這樣,當ret的時候,控制就交給了可執行代碼。
6.可執行代碼的字節編碼(攻擊代碼)的攻擊形式:一種方式是攻擊代碼會使用系統調用啓動一個外殼程序,給攻擊者提供一組操做系統函數。另外一種方式是,攻擊代碼會執行一些未受權的任務,修復對棧的破壞,而後第二次執行ret指令,(表面上)正常返回給調用者。
雖而後續的處理器系列引入了新的指令類型和格式,可是爲了保持向後兼容性,許多編譯器,包括GCC,都避免使用這些新特性。
咱們把浮點存儲模型,指令和傳遞規則的組合稱爲機器的浮點體系結構。x86的歷史中有多種浮點體系結構,目前有兩種還在使用:x87和SSE。
本章學習內容是彙編語言,如今直接寫彙編的機會很少了,但必定要能讀懂,信息安全的核心思惟方式「逆向」在這有很好很直接的體現,反彙編就是直接的逆向工程。
本章重點是3.7,但沒有3.1-3.6的基礎也是不行,若是想真正的提升動手能力,3.11如何用GDB調試彙編要好好練習一下,不過大多GDB技巧你們都會了。
3.1-3.7中練習,重點:3.1,3.3,3.5,3.6,3.9,3.14,3.15,3.16,3.22,3.23,3.27,3.29,3.30,3.33,3.34
p104, p105: X86 尋址方式經歷三代:
1 DOS時代的平坦模式,不區分用戶空間和內核空間,很不安全
2 8086的分段模式
3 IA32的帶保護模式的平坦模式
p106: ISA的定義,ISA須要你們能總結規律,觸類旁通,好比能對比學習ARM的ISA;PC寄存器要好好理解;
p107: gcc -S xxx.c -o xxx.s 得到彙編代碼,也能夠用objdump -d xxx 反彙編; 注意函數前兩條和後兩條彙編代碼,全部函數都有,創建函數調用棧幀,應該理解、熟記。
注意: 64位機器上想要獲得32代碼:gcc -m32 -S xxx.c
MAC OS中沒有objdump, 有個基本等價的命令otool
Ubuntu中 gcc -S code.c (不帶-O1) 產生的代碼更接近教材中代碼(刪除"."開頭的語句)
p108: 二進制文件能夠用od 命令查看,也能夠用gdb的x命令查看。
有些輸出內容過多,咱們可使用 more或less命令結合管道查看,也可使用輸出重定向來查看
od code.o | more
od code.o > code.txt
p109: gcc -S 產生的彙編中能夠把 以」.「開始的語句都刪除了再閱讀
p110: 瞭解Linux和Windows的彙編格式有點區別:ATT格式和Intel格式
p111: 表中不一樣數據的彙編代碼後綴
p112: 這幾個寄存器要深刻理解,知道它們的用處。esi edi能夠用來操縱數組,esp ebp用來操縱棧幀。
對於寄存器,特別是通用寄存器中的eax,ebx,ecx,edx,你們要理解32位的eax,16位的ax,8位的ah,al都是獨立的,咱們經過下面例子說明:
假定當前是32位x86機器,eax寄存器的值爲0x8226,執行完addw $0x8266, %ax指令後eax的值是多少?
解析:0x8226+0x826=0x1044c, ax是16位寄存器,出現溢出,最高位的1會丟掉,剩下0x44c,不要覺得eax是32位的不會發生溢出.
p113: 結合表,深刻理解各類 尋址方式;理解操做數的三種類型:當即數、寄存器、存儲器;掌握有效地址的計算方式 Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
p114: MOV至關於C語言的賦值」=「,注意ATT格式中的方向, 另外注意不能從內存地址直接MOV到另外一個內存地址,要用寄存器中轉一下。能區分MOV,MOVS,MOVZ,掌握push,pop
p115/p116: 棧幀與push pop; 注意棧頂元素的地址是全部棧中元素地址中最低的。
p117: 指針就是地址;局部變量保存在寄存器中。
p119: 結合表理解一下算術和邏輯運算, 注意目的操做數都是什麼類型
特別注意一下減法是誰減去誰
注意移位操做移位量能夠是當即數或%cl中的數
p123: 結合C語言理解一下控制部分,也就是分支(if/switch),循環語句(while, for)如何實現的。考驗你們觸類旁通的學習能力。控制中最核心的是跳轉語句:有條件跳轉p128(實現if,switch,while,for),無條件跳轉jmp(實現goto)
p124: 有條件跳轉的條件看狀態寄存器(教材上叫條件碼寄存器)
注意leal不改變條件碼寄存器
思考一下:CMP和SUB用在什麼地方
p125: SET指令根據t=a-b的結果設置條件碼
p127: 跳轉與標號
p130/p131: if-else 的彙編結構
p132/p133: do-while
p134/p135: while
p137/p138: for
p144/p145: switch
p149: IA32經過棧來實現過程調用。掌握棧幀結構,注意函數參數的壓棧順序.
p150/p151: call/ret; 函數返回值存在%eax中
p174: bt/frame/up/down :關於棧幀的gdb命令
使用gcc –S –o main.s main.c -m32
命令編譯成彙編代碼,以下代碼中的數字和函數名請自行修改以防與他人雷同
int g(int x) { return x + 3; } int f(int x) { return g(x); } int main(void) { return f(8) + 1; }
1.刪除gcc產生代碼中以"."開頭的編譯器指令,針對每條指令畫出相應棧幀的狀況
2.(選作)使用gdb的bt/frame/up/down 指令動態查看調用棧幀的狀況
1.反彙編代碼
2.彙編結果
3.使用GDB的堆棧跟蹤功能
還不會用這個功能,使用bt的時候爲空,想借本Linux的書再學習一下後再作一遍。
問題:
解決方法:
1. 1 typedef struct {
2. 2 int left;
3. 3 a_struct a[CNT];
4. 4 int right;
5. 5 } b_struct;
6. 6
7. 7 void test(int i, b_struct bp)
8. 8 {
9. 9 int n = bp->left + bp->right;
10.10 a_struct ap = &bp->a[i];
11.11 ap->x[ap->idx] = n;
12.12 }
查看文本打印
1. 1 000000
2. 2 0: 55 push %ebp
3. 3 1: 89 e5 mov %esp,%ebp
4. 4 3: 53 push %ebx
5. 5 4: 8b 45 08 mov 0x8(%ebp),%eax ;%eax=i
6. 6 7: 8b 4d 0c mov 0xc(%ebp),%ecx ;%ecx=bp
7. 7 a: 8b d8 1c imul $0x1c,%eax,%ebx ;%ebx=i*28
8. 8 d: 8d 14 c5 00 00 00 00 lea 0x0(,%eax,8),%edx ;%edx=8i;
9. 9 14: 29 c2 sub %eax,%edx ;%edx=7i;
10.10 16: 03 54 19 04 add 0x4(%ecx,%ebx,1),%edx ;%edx=7i+[bp+28i+4]
11.11 1a: 8b 81 c8 00 00 00 mov %0xc8(%ecx),%eax ;%eax=right
12.12 20: 03 01 add (%ecx),%eax ;%eax=right+left
13.13 22: 89 44 91 08 mov %eax,0x8(%ecx,%edx,4) ;[bp+4
7i+4[bp+28i+4]+0x8]=%eax
14.14 26: 5b pop %ebx
15.15 27: 5d pop %ebp
16.16 28: c3 ret
第22行,bp+47i+4[bp+28i+4]+0x8=bp+4+28i+4*[bp+28i+4]+4。這個地址就是ap->x[ap->idx]。bp+4是a[CNT]的首地址,bp+28i+4能夠看做是ap->idx。那麼咱們就很容推出sizeof(a_struct)大小爲28bytes。在a_struct中,idx的偏移爲0。而b.struct.a.struct.x[]偏移爲(bp+4+28i)+4,也進一步證實了sizeof(a_struct)的大小爲28bytes。right在b_struct中的偏移0xc8,也就是十進制的200,而a[CNT]的偏移爲4,則數組的總大小爲196,196/28=7,則CNT=7。
a_struct的大小爲28bytes,idx的大小爲4byte,剩下來24bytes都被x數組所佔用,故x數組中有6個元素。它的結構是:
查看文本打印
1.1 struct {
2.2 int idx;
3.3 int x[6];
4.4 }a_struct;
起初看到書中的彙編代碼時感到很是迷茫,由於跟上學期學到的彙編很不同,而後就將書中的一行代碼敲進了百度的搜索欄裏,發現咱們熟悉的是Intel彙編代碼格式,可是在Linux下的GCC、OBJDUMP等工具都是使用ATT格式,後來書上也有提到這個問題。所以首先要適應這種轉變。
看到網上有總結這兩種格式在如下方面是有區別的:
一、Intel省略了大小後綴;
二、Intel省略了%;
三、Intel使用DWORD PTR [ebp+12]而不是12(%ebp), %eax ;
四、帶有多個操做數時,順序相反;
五、ATT使用當即數前面要加 $ 符號。
考試和平時都發現了本身在Linux上的缺陷,還須要補一下怎麼使用。