應刁哥多年前的約稿,外加結合編譯原理系列連載,整理一下上一篇文章,寫一個簡易的匯編教程……
盡管謬誤,疏漏若干,但是本文的目的不在于培養學霸,而在于讓不會匯編的人簡單看看就能寫出個基本能用的匯編程序……
在程序設計中,我們知道一項基本原理:語言越高級,一般來講容易編寫,形式簡潔,可移植性強,效率低……
反過來,語言越低級,越難編寫,代碼越容易是又臭又長還看不懂,越難移植,但是效率會很高……
匯編就是比較低級的語言了,由于其不太好移植,匯編版本又多種多樣
因此,本著活學活用,到時候不會就查手冊的原則,本文不會太著重強調語法,語法以intel語法為主……
Chapter1:一些基本知識
首先,CPU有若干寄存器,在32位CPU中,形如eax,ebx,ecx,edx……64位CPU則為rax,rbx……
雖然寄存器這個名字很NB,其實它的本質上就是int,為了向下兼容的需要,一些老的程序,也有可能能在新型的CPU上運行(詳見DOS老游戲,有的能玩,有的不能玩)
以64位舉例:
64位寄存器rax,低32位是eax,eax低16位是ax,ax高8位叫ah,低8位叫al……
CPU只能直接對寄存器進行運算……因為電路結構就是那樣的……因此,所有的操作,都需要經過寄存器
譬如賦值:A = 1,實際上是 AX = 1; A = AX; 譬如 A = A + B,應該是 AX = A; AX += B; A = AX; 這樣
我是比較不求甚解的,99%功能(運算,判斷,指針)等,使用eax,ebx,ecx,edx都能搞定……于是咱們也就不說別的了……
Chapter2:匯編的基本語法:
匯編語言大概分為四個段,其中比較需要記住的就是代碼段和數據段……
用類似高級語言的思路來講,數據段用于定義一些全局變量,代碼段用于寫程序……
來看一個簡單的 Hello World(環境:yasm,64位)
global main
extern printf
section .data
ctrlout db '%s', 10, 0
hw db 'Hello World', 0
; this is a comment
section .text
main:
mov rax, 0
mov rdi, ctrlout
mov rsi, hw
call printf
mov rax, 0
ret
其中,剛開始global main 表示程序入口,由于這里直接調用了Linux的函數printf,之后需要gcc編譯,所以叫做main,如果只使用中斷輸出的話,可以搞成_start
之后extern意義和C++類似,表示這個函數在別處,叫編譯器以為他有就行……
section .data 表示數據段,后面是一些定義,以及初始化
注意這里定義的東西類型都像是指針一樣,ctrlout,hw 實際上是地址……
初始化必須搞的干凈一點,有一次調了半天不對,就是因為一個64位int,前4位沒有初始化成0……
之后 section .text 表示代碼段,這里調用C語言函數,網上說,64位匯編中,參數傳遞方式有了修改,大概是有4~5個寄存器專門保存參數,如果參數過多,再放入棧,rax存放棧中的參數個數……我們想printf("%s","Hello World"),就要調用兩個參數,于是,我們把第一個參數,字符串ctrlout的指針mov進rdi,hw mov進rsi,rax賦值為0 ,然后,調用……
(實現時,需要手冊自行查閱一下本機怎么搞)
函數的返回值在rax,于是return 0
然后 yasm -f elf64 XXX.asm
gcc -o XXX XXX.o
./XXX
一個嶄新的hello world 出現了……
Chapter3:匯編的一些簡單指令:
由于這是速成教程,選取一部分指令……要記住,CPU只能直接對寄存器進行運算……
mov:相當于賦值, mov eax,b 相當于eax = b;
注意:mov 后面的兩個東西至少要有一個是寄存器……
同時,和高級語言一樣,不能搞什么 mov 10,eax……
add:add eax,b 相當于eax+=b
sub:基本同上……
mul || imul:前面是無符號的乘,后面是有符號的乘,默認被乘數在AX,于是指令形如:mul BX,如果有溢出,則高位溢出到DX,記得備份DX的東西……
div || idiv:前面是無符號整除,后面是有符號整除,默認被除數是DX(高16位)和AX(低16位),因此,除法之前記得把DX清零,否則數會不對……
之后商在AX,余數在DX
指令形如:div BX
push/pop:每個程序都有棧,或者是在程序中定義堆棧段,或者使用默認棧
push eax,表示把eax放進棧里,pop ebx,表示取出棧頂,放在ebx中……
push和pop異常重要,一個重要作用就是保護寄存器,譬如DX中有內容,但是現在要做乘法,沒準會破壞DX(見上),于是,先PUSH DX,然后,乘法,然后POP DX,又好比你要調用一個函數,但是寄存器里有有用的信息,不保證這個函數不會破壞,于是,把所有寄存器先push進去,運行函數,然后再pop出來,這是常用技巧……
另外也可以用來給函數傳參數,傳時倒序壓入,用時順序取出,棧,先進后出,你們懂的……
Chapter4:if以及循環
首先我們要記得,被Dijkstra老爺子罵的一文不值的goto……
jmp語法和C++里的goto基本一樣
start:
XXX
jmp start
然后,匯編沒有if then,但是有條件goto
有個語句叫做cmp
cmp X,Y (老原則,這兩個得有一個寄存器……如果和常數比較,常數必須在后面)
傳說中具體實現是減法……
這個的效果是可能改變若干標識位的值……譬如:0標識,進位標識,溢出標識……等等……
然后,有若干指令
jl XX(l==less) 相當于 if (x<y) goto XX; 下同
jg XX(g==greater)
jle XX(ne==less or equal)
jge XX(ge==greter or equal)
jnle XX(n==no->nle==g)
jnge XX(n==no->nge==l)
有了if then,各種運算,我們就能做出來&&,||的邏輯
有了if then,我們也能做出循環……
很多的功能就有了……
Chapter5:實戰
看看
int main() {
int a,b,c;
input(a);
input(b);
if (a < b) {
c = a;
} else {
c = b;
}
print(c);
}
用匯編翻譯出來啥樣:
global main
extern printf
extern scanf
section .data
ctrlout db '%lld', 10, 0
ctrlin db '%lld', 0
_a db 0,0,0,0,0,0,0,0
_b db 0,0,0,0,0,0,0,0
_c db 0,0,0,0,0,0,0,0
section .text
main:
mov rax, 0
mov rdi, ctrlin
mov rsi, _a
call scanf
mov rax, 0
mov rdi, ctrlin
mov rsi, _b
call scanf
MOV rax, [_a]
CMP rax, [_b]
jl @0
jmp @2
@0:
MOV rax, [_a]
MOV [_c], rax
jmp @1
@2:
MOV rax, [_b]
MOV [_c], rax
@1:
mov rax, 0
mov rdi, ctrlout
mov rsi, [_c]
call printf
mov rax, 0
ret
Chapter6:其它
由于現實生活中,我需要寫匯編時候實在是少,因此也沒啥經驗……盡管借助手冊,可以對付著寫一點匯編代碼,但是那是不科學的……對我來講,匯編告訴我們的一些底層的東西更有價值,可以在高級語言中有所體現:譬如一些表達式的寫法,可以考慮寫的更科學一些,譬如 c = a / b, q = a % b,記得寫成 c = a / b; q = a - c * b,譬如靈活使用switch,等等……