程式的機器級表示
資料傳送指令
将資料從一個位置複制到另一個位置的指令是最頻繁使用的指令。
算術邏輯指令
控制指令
目的:
條件語句、循環語句和分支語句,要求有條件的進行,根據資料測試的結果來決定操作執行的順序。機器代碼提供兩種基本的低級機制來實作有條件的行為:測試資料值,然後根據測試的結果來改變控制流或者資料流
控制指令-條件碼
CPU維護着一組單個位的條件碼寄存器,他們描述了最近的算術或者邏輯操作的屬性。可以檢測這些寄存器來執行條件分支指令
最常用的條件碼:
CF:進位标志。最近的操作使最高位産生了進位。可以用來檢查無符号操作數的溢出
ZF:零标志。最近的操作得出的結果為0.
SF:符号标志。最經的操作得到的結果為負數。
OF:溢出标志。最近的操作導緻一個補碼溢出——正溢出或者負溢出
比較和測試指令
有兩類指令隻設定條件碼而不改變任何其他寄存器。
CMP指令根據兩個操作數之間的差來設定條件碼。
TEST指令的行為同and一樣。典型的用法是,testl %eax,%eax用來檢查%eax是負數、零還是正數
控制指令
Do-while循環
源代碼
do{
body-statement
}while(test-expr)
翻譯成彙編的僞代碼
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
Do-while循環-舉例
真實代碼:
int fact_do(int n)
{
int result = 1;
do
{
result *= n;
n = n-1;
}while(n > 1);
return result;
}
彙編代碼:
_fact_do:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取n
movl $1, %eax//寫入1
L5:
imull %edx, %eax// result *= n
decl %edx// n = n-1
cmpl $1, %edx//判斷n大于1
jg L5//是的話繼續
popl %ebp
ret
While循環
源代碼
while(test-expr)
body-statement
翻譯成彙編的僞代碼
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
done:
While循環-舉例
源代碼:
int fact_while(int n)
{
int val = 0;
while(x)
{
val ^= x;
x >>=1;
}
return val & 0x1;
}
彙編代碼:
_fact_while:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取n
movl $0, %eax//val = 0
testl %edx, %edx//x = 0?
je L25//是的話,return
L23:
xorl %edx, %eax
shrl %edx
jne L23//循環
L25:
andl $1, %eax
popl %ebp
ret
For循環
源代碼:
for(init-expr;
test-expr;
update-expr)
body-statement;
翻譯成彙編的僞代碼:
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if(t)
goto loop;
done:
For循環-舉例
源代碼:
int sum(int x)
{
int sum = 0;
int i;
for(i = 0; i< x; i++)
{
if(i & 1)
continue;
sum += i;
}
return sum;
}
彙編代碼:
_sum:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx//讀取x
movl $0, %eax//sum=0
movl $0, %edx//i = 0
cmpl %ecx, %eax//判斷 0>= x?
jge L33//如果成立,結束
L31:
testb $1, %dl//測試 1 and i
jne L29//如果不為0,跳轉L29
addl %edx, %eax//否則sum +=i
L29:
incl %edx//i++
cmpl %ecx, %edx//判斷 i < x?
jl L31//如果成立,繼續循環
L33:
popl %ebp
ret
Switch語句
int swichtest(int a, int b, int c)
{
int answer;
switch(a)
{
case 5:
c = b^15;
case 0:
answer = c +115;
break;
case 2:
case 7:
answer = (c + b) << 2;
break;
case 4:
answer = a;
break;
default:
answer = b;
}
return answer;
}
int swichtest_impl(int a, int b, int c)
{
static void *jt[8] = {
&&loc_A, &&loc_def, &&loc_B, &&loc_def,
&&loc_C , &&loc_D, &&loc_def , &&loc_B
};
int answer;
if(a > 7)
goto loc_def;
goto *jt[a];
loc_def :
answer = b;
goto done;
loc_D :
c = b^15;
loc_A :
answer = c +115;
goto done;
loc_B :
answer = (c + b) << 2;
goto done;
loc_C :
answer = a;
goto done;
done:
return answer;
}
_swichtest:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取a
movl 12(%ebp), %ecx//讀取b
movl 16(%ebp), %eax//讀取c
cmpl $7, %edx//如果 a大于 7,default
ja L35//跳轉default
jmp *L36(,%edx,4)//按照數組跳轉
L30: //case 5
movl %ecx, %eax
xorl $15, %eax//順延到下一個條件
L31: //case 0
addl $115, %eax
jmp L29//傳回
L33: //case 2,7
addl %ecx, %eax
sall $2, %eax
jmp L29//傳回
L34: //case 4
movl %edx, %eax
jmp L29//傳回
L35: //default
movl %ecx, %eax
L29:
popl %ebp
ret
L36:
.long L31
.long L35
.long L33
.long L35
.long L34
.long L30
.long L35
.long L33
.text
If-else語句
模闆
if(test-expr)
then-statement
else
else-statement
一般情況
if(!test-expr)
goto false;
v = then-expr;
goto done;
false:
v = else-expr;
done:
使用條件傳送語句的格式
vt = then-expr;
v = else-expr;
t = test-expr;
if(t) v = vt;
If-else-一般情況舉例
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取x
movl 12(%ebp), %eax//讀取y
cmpl %eax, %edx//判斷x>=y?
jge L2//是的話,跳轉
subl %edx, %eax//y - x
jmp L3
L2:
subl %eax, %edx//x - y
movl %edx, %eax//将結果存到結果寄存器
L3:
popl %ebp
ret
If-else-使用條件傳送語句
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ecx //讀取x
movl 12(%ebp), %edx //讀取y
movl %edx, %ebx
subl %ecx, %ebx //y - x
movl %ecx, %eax //
subl %edx, %eax //x - y
cmpl %edx, %ecx //判斷x < y
cmovl %ebx, %eax//是的話傳回y - x
popl %ebx
popl %ebp
ret
編譯器在什麼情況下選擇什麼方式
在分支計算量小的情況下,基于條件資料傳送的代碼比基于條件控制轉移的代碼性能要好。
這本書後面會介紹到處理器通過使用流水線來獲得高性能,在碰到條件控制轉移的時候,會采用精密的分支預測邏輯來猜測跳轉指令的執行與否,但是有時候這個猜測是不可靠的,進而浪費了程式執行的時間。但是使用條件資料傳送的方式隻需要檢查條件碼,要麼更新目的寄存器,要麼保持不變。
編譯器會根據分支預測所需要的時間和條件傳送浪費的計算上進行權衡,編譯成哪種邏輯才是合适的。
If-else-例外
int cread(int *xp)
{
return (xp? *xp : 0);
}
_cread:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl $0, %eax
testl %edx, %edx
je L9
movl (%edx), %eax
L9:
popl %ebp
ret
我們看到編譯器依然選擇了使用分支跳轉的方式進行編譯的。原因是即使當xp測試為空的時候,cmovne對xp的間接引用還是發生了,導緻一個間接引用空指針的錯誤
.net程式集加載流程
對于一個已編譯好的.NET程式集,Windows作業系統是如何啟動執行的呢?日常使用中我們發現對于托管的和非托管的程式集編譯器都會吧程式集編譯成以.exe或.dll等為擴充名的檔案,可見Windows加載器并沒有區分是托管還是非托管的程式集,而且我們也知道對非托管的程式集是在編譯器直接編譯成了機器碼,自然可以由CPU直接執行,而托管的.NET 程式集是包含複雜結構的MSIL代碼,執行時會使用JIT即時編譯器将IL代碼編譯成機器碼,再由CPU執行,當然這期間還需要執行其它許多的工作,如加載CLR、執行初始化等工作,那麼這些是怎麼自動實作的呢?
首先我們要清楚的是對于托管還是非托管程式集,他們在編譯器執行編譯時都會編譯成一個特殊的檔案格式,即PE檔案(可移植可執行檔案格式),作業系統加載器通過加載這樣的PE檔案來執行程式集的。可以這麼說吧,無論是托管程式還是非托管程式他們實際上都是編譯成這樣的PE檔案(隻是有部分内容不一樣而已)。
然後這個PE檔案會訓示如何執行托管程式集和非托管程式集,加載器首先會查找到PE頭中的AddressOfEntryPoint域,這個域訓示PE檔案的入口點位置,在.NET程式集中是指向.text段中的CLR頭--〉包含一個結構IMAGE_COR20_HEADER—包含許多資訊如托管代碼應用程式的入口點,目标CLR的主版本号和從版本号,以及程式集的強名稱簽名等--〉Windows加載器根據這個資料結構決定加載哪個版本的CLR以及一些基本的程式集資訊。在.text段中還包含了程式集的中繼資料表,MSIL以及非托管啟動存根代碼,而非托管啟動存根代碼包好了由Windows加載器執行役啟動PE檔案執行的代碼,結構如圖所示。
1、使用者執行一個.NET程式集;
2、Windows加載器檢視AddressOfEntryPoint域,并找到PE映像檔案的.text段;
3、位于AddressOfEntryPoint位置上的位元組隻是一個JMP(跳轉)指令,這個指令跳轉到mscoree.dll中的一個導入函數;
4、将執行控制轉移到mscoree.dll中的_CorExeMain中,這個函數将啟動CLR并把執行控制轉移到程式集的入口點。
程式的機器級表示
資料傳送指令
将資料從一個位置複制到另一個位置的指令是最頻繁使用的指令。
算術邏輯指令
控制指令
目的:
條件語句、循環語句和分支語句,要求有條件的進行,根據資料測試的結果來決定操作執行的順序。機器代碼提供兩種基本的低級機制來實作有條件的行為:測試資料值,然後根據測試的結果來改變控制流或者資料流
控制指令-條件碼
CPU維護着一組單個位的條件碼寄存器,他們描述了最近的算術或者邏輯操作的屬性。可以檢測這些寄存器來執行條件分支指令
最常用的條件碼:
CF:進位标志。最近的操作使最高位産生了進位。可以用來檢查無符号操作數的溢出
ZF:零标志。最近的操作得出的結果為0.
SF:符号标志。最經的操作得到的結果為負數。
OF:溢出标志。最近的操作導緻一個補碼溢出——正溢出或者負溢出
比較和測試指令
有兩類指令隻設定條件碼而不改變任何其他寄存器。
CMP指令根據兩個操作數之間的差來設定條件碼。
TEST指令的行為同and一樣。典型的用法是,testl %eax,%eax用來檢查%eax是負數、零還是正數
控制指令
Do-while循環
源代碼
do{
body-statement
}while(test-expr)
翻譯成彙編的僞代碼
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
Do-while循環-舉例
真實代碼:
int fact_do(int n)
{
int result = 1;
do
{
result *= n;
n = n-1;
}while(n > 1);
return result;
}
彙編代碼:
_fact_do:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取n
movl $1, %eax//寫入1
L5:
imull %edx, %eax// result *= n
decl %edx// n = n-1
cmpl $1, %edx//判斷n大于1
jg L5//是的話繼續
popl %ebp
ret
While循環
源代碼
while(test-expr)
body-statement
翻譯成彙編的僞代碼
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
done:
While循環-舉例
源代碼:
int fact_while(int n)
{
int val = 0;
while(x)
{
val ^= x;
x >>=1;
}
return val & 0x1;
}
彙編代碼:
_fact_while:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取n
movl $0, %eax//val = 0
testl %edx, %edx//x = 0?
je L25//是的話,return
L23:
xorl %edx, %eax
shrl %edx
jne L23//循環
L25:
andl $1, %eax
popl %ebp
ret
For循環
源代碼:
for(init-expr;
test-expr;
update-expr)
body-statement;
翻譯成彙編的僞代碼:
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if(t)
goto loop;
done:
For循環-舉例
源代碼:
int sum(int x)
{
int sum = 0;
int i;
for(i = 0; i< x; i++)
{
if(i & 1)
continue;
sum += i;
}
return sum;
}
彙編代碼:
_sum:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx//讀取x
movl $0, %eax//sum=0
movl $0, %edx//i = 0
cmpl %ecx, %eax//判斷 0>= x?
jge L33//如果成立,結束
L31:
testb $1, %dl//測試 1 and i
jne L29//如果不為0,跳轉L29
addl %edx, %eax//否則sum +=i
L29:
incl %edx//i++
cmpl %ecx, %edx//判斷 i < x?
jl L31//如果成立,繼續循環
L33:
popl %ebp
ret
Switch語句
int swichtest(int a, int b, int c)
{
int answer;
switch(a)
{
case 5:
c = b^15;
case 0:
answer = c +115;
break;
case 2:
case 7:
answer = (c + b) << 2;
break;
case 4:
answer = a;
break;
default:
answer = b;
}
return answer;
}
int swichtest_impl(int a, int b, int c)
{
static void *jt[8] = {
&&loc_A, &&loc_def, &&loc_B, &&loc_def,
&&loc_C , &&loc_D, &&loc_def , &&loc_B
};
int answer;
if(a > 7)
goto loc_def;
goto *jt[a];
loc_def :
answer = b;
goto done;
loc_D :
c = b^15;
loc_A :
answer = c +115;
goto done;
loc_B :
answer = (c + b) << 2;
goto done;
loc_C :
answer = a;
goto done;
done:
return answer;
}
_swichtest:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取a
movl 12(%ebp), %ecx//讀取b
movl 16(%ebp), %eax//讀取c
cmpl $7, %edx//如果 a大于 7,default
ja L35//跳轉default
jmp *L36(,%edx,4)//按照數組跳轉
L30: //case 5
movl %ecx, %eax
xorl $15, %eax//順延到下一個條件
L31: //case 0
addl $115, %eax
jmp L29//傳回
L33: //case 2,7
addl %ecx, %eax
sall $2, %eax
jmp L29//傳回
L34: //case 4
movl %edx, %eax
jmp L29//傳回
L35: //default
movl %ecx, %eax
L29:
popl %ebp
ret
L36:
.long L31
.long L35
.long L33
.long L35
.long L34
.long L30
.long L35
.long L33
.text
If-else語句
模闆
if(test-expr)
then-statement
else
else-statement
一般情況
if(!test-expr)
goto false;
v = then-expr;
goto done;
false:
v = else-expr;
done:
使用條件傳送語句的格式
vt = then-expr;
v = else-expr;
t = test-expr;
if(t) v = vt;
If-else-一般情況舉例
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//讀取x
movl 12(%ebp), %eax//讀取y
cmpl %eax, %edx//判斷x>=y?
jge L2//是的話,跳轉
subl %edx, %eax//y - x
jmp L3
L2:
subl %eax, %edx//x - y
movl %edx, %eax//将結果存到結果寄存器
L3:
popl %ebp
ret
If-else-使用條件傳送語句
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ecx //讀取x
movl 12(%ebp), %edx //讀取y
movl %edx, %ebx
subl %ecx, %ebx //y - x
movl %ecx, %eax //
subl %edx, %eax //x - y
cmpl %edx, %ecx //判斷x < y
cmovl %ebx, %eax//是的話傳回y - x
popl %ebx
popl %ebp
ret
編譯器在什麼情況下選擇什麼方式
在分支計算量小的情況下,基于條件資料傳送的代碼比基于條件控制轉移的代碼性能要好。
這本書後面會介紹到處理器通過使用流水線來獲得高性能,在碰到條件控制轉移的時候,會采用精密的分支預測邏輯來猜測跳轉指令的執行與否,但是有時候這個猜測是不可靠的,進而浪費了程式執行的時間。但是使用條件資料傳送的方式隻需要檢查條件碼,要麼更新目的寄存器,要麼保持不變。
編譯器會根據分支預測所需要的時間和條件傳送浪費的計算上進行權衡,編譯成哪種邏輯才是合适的。
If-else-例外
int cread(int *xp)
{
return (xp? *xp : 0);
}
_cread:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl $0, %eax
testl %edx, %edx
je L9
movl (%edx), %eax
L9:
popl %ebp
ret
我們看到編譯器依然選擇了使用分支跳轉的方式進行編譯的。原因是即使當xp測試為空的時候,cmovne對xp的間接引用還是發生了,導緻一個間接引用空指針的錯誤
.net程式集加載流程
對于一個已編譯好的.NET程式集,Windows作業系統是如何啟動執行的呢?日常使用中我們發現對于托管的和非托管的程式集編譯器都會吧程式集編譯成以.exe或.dll等為擴充名的檔案,可見Windows加載器并沒有區分是托管還是非托管的程式集,而且我們也知道對非托管的程式集是在編譯器直接編譯成了機器碼,自然可以由CPU直接執行,而托管的.NET 程式集是包含複雜結構的MSIL代碼,執行時會使用JIT即時編譯器将IL代碼編譯成機器碼,再由CPU執行,當然這期間還需要執行其它許多的工作,如加載CLR、執行初始化等工作,那麼這些是怎麼自動實作的呢?
首先我們要清楚的是對于托管還是非托管程式集,他們在編譯器執行編譯時都會編譯成一個特殊的檔案格式,即PE檔案(可移植可執行檔案格式),作業系統加載器通過加載這樣的PE檔案來執行程式集的。可以這麼說吧,無論是托管程式還是非托管程式他們實際上都是編譯成這樣的PE檔案(隻是有部分内容不一樣而已)。
然後這個PE檔案會訓示如何執行托管程式集和非托管程式集,加載器首先會查找到PE頭中的AddressOfEntryPoint域,這個域訓示PE檔案的入口點位置,在.NET程式集中是指向.text段中的CLR頭--〉包含一個結構IMAGE_COR20_HEADER—包含許多資訊如托管代碼應用程式的入口點,目标CLR的主版本号和從版本号,以及程式集的強名稱簽名等--〉Windows加載器根據這個資料結構決定加載哪個版本的CLR以及一些基本的程式集資訊。在.text段中還包含了程式集的中繼資料表,MSIL以及非托管啟動存根代碼,而非托管啟動存根代碼包好了由Windows加載器執行役啟動PE檔案執行的代碼,結構如圖所示。
1、使用者執行一個.NET程式集;
2、Windows加載器檢視AddressOfEntryPoint域,并找到PE映像檔案的.text段;
3、位于AddressOfEntryPoint位置上的位元組隻是一個JMP(跳轉)指令,這個指令跳轉到mscoree.dll中的一個導入函數;
4、将執行控制轉移到mscoree.dll中的_CorExeMain中,這個函數将啟動CLR并把執行控制轉移到程式集的入口點。