文章目錄
- 概要
- 一、寄存器
- 1.1、8086寄存器
- 1.2、通用寄存器
- 1.3、擴展寄存器
- 二、指令集
- 三、x86指令集常見指令使用說明
- 四、匯編
- 4.1、匯編語法
- 4.2、nsam匯編
- 五、參考
概要
在對學習Go的過程中,涉及到了匯編,因此對X86系列CPU的背景、寄存器、匯編指令做了一番了解,僅以本文作為總結,便于后續查看。
x86架構始于Intel在1978年推出的8086 CPU(典中典),它是從Intel 8008處理器中發展而來的,而8008則是發展自Intel 4004的。8086在三年后為IBM PC所選用,之后x86便成為了電腦的標配(當然,現在有些電腦采用ARM架構的CPU,比如mac m1),成為了非常成功的CPU架構。
除了Intel公司,其他公司也有制造x86架構的CPU,比如IBM、IDT以及Transmeta,但Intel以外最成功的制造商要數AMD了,其早先產品Athlon系列處理器的市場份額僅次于Intel的Pentium。
值得注意的是8086是16位處理器,直到1985年32位的80386(IA-32)才被開發,接著推出了一系列針對32位架構細微改進的CPU型號,直到2003年AMD對于這個架構進行了64位的擴展,并命名為AMD64,后來英特爾也推出了與之兼容的處理器,并命名為Intel 64。兩者一般被統稱為x86-64或x64,開創了x86的64位時代。
一、寄存器
正如概要
中所說,x86系列發展從16、32到64位,寄存器也隨之發展。
1.1、8086寄存器
8086中寄存器總共為 14 個,且均為 16 位。
- 通用寄存器
寄存器 | 含義 |
---|---|
AX、BX、CX、DX 也稱為數據寄存器 | AX (Accumulator):累加寄存器,也稱之為累加器; BX (Base):基地址寄存器; CX (Count):計數器寄存器; DX (Data):數據寄存器; |
SP、BP 也稱為指針寄存器 | SP (Stack Pointer):堆棧指針寄存器,存放棧頂偏移地址,與SS配合使用,即棧頂地址=SS給出的棧段首地址+SP中偏移地址; BP (Base Pointer):基指針寄存器,一般存放函數棧幀起始指針; |
SI、DI 也稱為變址寄存器 | SI (Source Index):源變址寄存器; DI (Destination Index):目的變址寄存器; |
ps:AX可以通過AH操作其高8位,AL操作其低8位(這樣可以當做兩個8位寄存器使用了),BX、CX、DX同理。
- 控制寄存器
寄存器 | 含義 |
---|---|
IP | IP (Instruction Pointer):指令指針寄存器;存放下一條指令的偏移地址,搭配CS使用,即下一條指令的地址=CS給出代碼段基址<<4+IP中偏移地址 |
FLAG | 標志寄存器 |
ps:偏移地址也稱偏移量,就是計算機里的內存分段后,在段內某一地址相對于段首地址(段地址)的偏移量。
- 段寄存器
寄存器 | 含義 |
---|---|
CS | CS (Code Segment):代碼段寄存器; |
DS | DS (Data Segment):數據段寄存器; |
SS | SS (Stack Segment):堆棧段寄存器; |
ES | ES (Extra Segment):附加段寄存器; |
DS 寄存器和 ES 寄存器都屬于段寄存器,其實它們和 CS 寄存器以及 SS 寄存器用起來區別不大,既然是段寄存器的話,自然它們存放的就是某個段地址了 。我們已經知道,如果 CPU 要訪問一個內存單元時,我們必須要提供一個指向這個內存單元的物理地址給 CPU ,而我們也知道在 8086 CPU 中,物理地址是由段地址左移 4 位,然后加上偏移地址形成的,所以,我們也就只需要提供段地址和偏移地址即 OK 。8086 CPU 呢,提供了一個 DS 寄存器,并且通常都是通過這個 DS 段寄存器來存放要訪問的數據的段地址 。DS(Data Segment):很顯然,DS 中存放的是數據段的段地址 。但是這里不得不再點一下,那就是我們對段的支持是在 CPU 上體現的,而不是在內存中實現了段,所以事實上我們使用的段其實是一個邏輯概念,即是我們自己定義的,再說白了,我定義一個段,我說它是數據段那它就是數據段,我說它是代碼段那么它就是代碼段,它們其實都是一塊連續的內存而已,我們只要能定位不同段即可,至于為什么要區分為數據段和代碼段,很明顯,是用來給我們編程提供方便的,即我們在自己的思想上或者說是編碼習慣上規定,數據放數據段中,代碼放代碼段中 。而我們在使用數據段的時候,為了方便或者說是代碼的編寫方便起見,我們一般把數據段的段地址放在 DS 寄存器中,當然,如果你硬要覺得 DS 不順眼,那你可以換個 ES 也是一樣的,至于 ES(Extra Segment) 段寄存器的話,自然,是一個附加段寄存器,如果再說得過分點,就當它是個擴展吧,當你發現,你幾個段寄存器不夠用的時候,你可以考慮使用 ES 段寄存器,在使用方式上,則和其他的段寄存器沒什么區別 。
1.2、通用寄存器
通用寄存器在匯編指令中是最多用到了,所以這里多說一嘴。
最初的8086有8個16位通用寄存器從%ax~%sp,等開發出32位時,這8個寄存器也被擴展成32位寄存器,標號命名以%e(Extense)開頭從%eax ~ %esp,后來又了64位的,這8個寄存器也被擴展成64位寄存器,標號命名以%r(Register)開頭從%rax ~ %rsp,另外還增加了8個寄存器,它們的標號命名規則為%r8 ~%r15。
x64通用寄存器:
64位寄存器 | 操作低32位 | 操作低16位 | 操作低8位 |
---|---|---|---|
rax | eax | ax | al |
… | … | … | rax與rsp之間的寄存器同理 |
rsp | esp | sp | spl |
r8 | r8d | r8w | r8b |
… | … | … | r8與r15之間的同理 |
r15 | r15d | r15w | r15b |
另外rax、rbx、rcx 和 rdx 的高 8 位仍可通過 ah、bh、ch、dh訪問。
1.3、擴展寄存器
Intel為了增強CPU對多媒體信息的處理能力,提高CPU處理3D圖形、視頻和音頻信息的能力,在1996年推出了多媒體擴展指令集MMX(Multi-Media Extension),為了支持該指令集,增加了[MM0,MM7]8個64位的寄存器。
由于MMX指令集只局限于整數的運算,反響并不好。在1999年Intel推出SSE指令集(Streaming SIMD Extension),支持整數和浮點數運算,從此多了[XMM0,XMM15]共16個128位的寄存器。后續又接連推出了SSE2到SSE4.2作為補充的SSE家族,還有AVX、AVX-512、AMX等指令指令集家族,相應的也增加了[YMM0,YMM15]16個256位的寄存器和[ZMM0,ZMM31]32個512位的寄存器
ps:當然,x86系列寄存器肯定不止這些,本文只介紹了相對常用的,有興趣的可到Intel官網了解
傳送門
二、指令集
指令集,就是CPU中用來計算和控制計算機系統的一套指令的集合,而每一種新型的CPU在設計時就規定了一系列與其他硬件電路相配合的指令系統。而指令集的先進與否,也關系到CPU的性能發揮,它也是CPU性能體現的一個重要標志。
通俗的理解,指令集就是CPU能認識的語言,指令集運行于一定的微架構之上,不同的微架構可以支持相同的指令集,比如Intel和AMD的CPU的微架構是不同的,但是同樣支持X86指令集,這很容易理解,指令集只是一套指令集合,一套指令規范,具體的實現,仍然依賴于CPU的翻譯和執行。
指令集一般分為RISC(精簡指令集 Reduced Instruction Set Computer)和CISC(復雜指令集Complex Instruction Set Computer)。Intel X86的第一個CPU定義了第一套指令集,后來一些公司發現很多指令并不常用,所以決定設計一套簡潔高效的指令集,稱之為RICS指令集(ARM指令集),從而將原來的Intel X86指令集定義為CISC指令集。兩者的使用場合不一樣,對于復雜的系統,CISC更合適,否則,RICS更合適,且低功耗。
-
X86指令集
X86指令集是Intel為其第一塊16位CPU(i8086)專門開發的,IBM1981年推出的世界第一臺PC機中的CPU—i8088(i8086簡化版)使用的也是X86指令,同時電腦中為提高浮點數據處理能力而增加的X87芯片系列數學協處理器則另外使用X87指令,以后就將X86指令集和X87指令集統稱為X86指令集。常見的如mov、add、sub、pop、push等. -
MMX指令集
1996年Intel的MMX(AMD認為這是矩陣數學擴展Matrix Math Extensions的縮寫,但大多數時候都被當成Multi-Media Extension,而Intel從來沒有官方宣布過詞源)技術出現。盡管這項新的科技得到廣泛宣傳,但它的精髓是非常簡單的:MMX定義了八個64位SIMD寄存器,與Intel Pentium處理器的FPU堆棧有相重疊。不幸的是,這些指令無法非常簡單地對應到由原來C編譯器所產生的腳本中。MMX也只局限于整數的運算。這項技術的缺點導致MMX在它早期的存在有輕微的影響。現今,MMX通常是用在某些2D影片應用程序中。 -
SSE指令集
由于MMX指令并沒有帶來3D游戲性能的顯著提升,所以,1999年Inter公司在Pentium III CPU產品中推出了數據流單指令序列擴展指令(SSE),兼容MMX指令。SSE為Streaming SIMD Extensions的縮寫,如同其名稱所表示的,是一種SSE指令包括了四個主要的部份:單精確度浮點數運算指令、整數運算指令(此為MMX之延伸,并和MMX使用同樣的寄存器)、Cache控制指令、和狀態控制指令。
在Pentium 4 CPU中,Inter公司開發了新指令集SSE2。SSE2指令一共144條,包括浮點SIMD指令、整形SIMD指令、SIMD浮點和整形數據之間轉換、數據在MMX寄存器中轉換等幾大部分。其中重要的改進包括引入新的數據格式,如:128位SIMD整數運算和64位雙精度浮點運算等。相對于SSE2,SSE3又新增加了13條新指令,此前它們被統稱為pni(prescott new instructions)。13條指令中,一條用于視頻解碼,兩條用于線程同步,其余用于復雜的數學運算、浮點到整數轉換和SIMD浮點運算。SSE4增加了50條新的增加性能的指令,這些指令有助于編譯、媒體、字符/文本處理和程序指向加速。
這里說下 SIMD(Single Instruction Multiple Data),單指令多數據,其不是指令集,而是一種思想,MMX、SSE、AVX等指令集就是這種思想的具體實現。它的精髓就是要通過一條指令,實現對寄存器上多組數據并行操作。比如arr:=[]int64{1,2,3,4},計算下標1與下標3之和,下標2與下標4之和,如果使用x86指令集,就要執行add rax,rbx兩次,而SSE指令直接addpd xmm0,xmm1就搞定了。
ps:AVX、AVX-512、AMX等指令集,就不一一介紹了。
傳送門
三、x86指令集常見指令使用說明
x86指令大全傳送門。
本章節最好結合實戰文章來看1和2。
匯編語法有很多,主流如Intel 語法(源操作數在后,目標操作數在前)、AT&T 語法(源操作數在前,目標操作數在后)、本節解釋指令示例用Intel 語法,x86-64環境
- mov
mov指令將源操作數(可以是寄存器的內容、內存中的內容或立即數)復制到目的操作數(寄存器中或內存上)。mov不能用于直接從內存復制到內存。
語法如下:
mov <reg>,<reg>
mov <reg>,<mem>
mov <mem>,<reg>
mov <reg>,<const>
mov <mem>,<const>
示例如下:
mov rax, rbx; 將rbx的值拷貝到rax
mov rax, 0x5; 將立即數5加載到rax寄存器
mov qword ptr [rsp+0x18], 0x1;將立即數1(qword ptr表示占8字節)加載到地址rsp+0x18到rsp+0x20之間的內存區域
mov qword ptr [rsp+0x18], rax; 將rax寄存器的內容(qword ptr表示大小為8字節)加載到地址rsp+0x18到rsp+0x20之間的內存區域
- lea - Load effective address
lea是一個載入有效地址的指令,將源操作數表示的地址載入到目的操作數(寄存器)中。
語法如下:
lea <reg>,<mem>
示例如下:
lea rbp, ptr [rsp-0x10]; 將地址rsp-0x10到rsp-0x8之間內存區域存儲的8字節地址(指針)加載到rbp寄存器
- push
push指令擴棧后將操作數壓入棧頂。通過第一章寄存器可知,一般棧頂由rsp寄存器確定。棧一般從高地址向低地址擴張。
語法如下:
push <reg>
push <mem>
push <const>
示例如下:
push rbp; 將rbp寄存器的內容壓入棧,x86-64下等價于sub rsp 0x8; mov rsp, rbp,也就是說push指令改變棧大小,減8字節
- pop
pop指令與push指令相反,將棧頂內容彈出,并縮棧。
語法如下:
pop <reg>
pop <mem>
示例如下:
pop rbp; 將棧頂內容寫入rbp寄存器,x86-64下等價于 mov rbp,rsp; add rsp 0x8 也就是說pop指令改變棧大小,增8字節
- call
call指令跳轉到標簽處,其主要工作是擴棧(字節數為指針大小)后將call下一條指令地址寫入棧頂(下一條指令由rip寄存器會自動裝載的),并跳轉到標簽。
語法如下:
call <label>
示例如下:
call $main.sub; 調用sub函數,等價于push rip,jmp <label>; 也就是說call指令也會改變棧大小,x86-64下減8字節
- ret
ret指令指令實現子程序的返回機制, 其主要工作是將棧頂內容寫入rip寄存器,并縮棧,后續cpu繼續執行rip的指令。這樣就和call實現的函數的調用與返回。
ret; 等價于pop rip;也就是說ret指令也會改變棧大小,x86-64下增加8字節
- cmp
cmp指令比較源操作數與目的操作數大小,比較結果設置到FLAG寄存器中。 - jxx條件跳轉,根據FLAG寄存器特定標志位來決定是否跳轉
je或jz 如果相等(等于零)跳轉;
jne或jnz 如果不相等(等于零)跳轉;
js 如果為負值跳轉;
jns 如果不為負值跳轉;
jg 如果大于跳轉;
jge 如果大于等于跳轉;
jl 如果小于跳轉;
jle 如果小于等于跳轉;
…
運算指令
add rax,rbx;加法,rbx+rax,并將結果保存到rax中。
sub rax,rbx;減法,rbx-rax并將結果保存到rax中。
inc rax; 自增,rax=rax+1;
dex rax;自減,rax=rax-1
imul rax,rbx;乘法
idiv rax,rbx;除法
and、or、xor、not、neg 都是位運算,依次是與、或、異或、非、求補
四、匯編
提到匯編,不得不說匯編器和匯編語法。
4.1、匯編語法
匯編器 | 默認語法 | 典型應用場景? |
---|---|---|
NASM | Intel | Windows/Linux 跨平臺 |
GNU Assembler(GAS、AS) | AT&T | Linux 內核及 GCC 工具鏈 |
MASM | Intel | Windows 平臺開發 |
YASM | Intel | 跨平臺優化 |
go tool asm? | plan9 | go編譯過渡階段 |
匯編語法也各有特色,簡單說下其核心差異:
差異\語法 | Intel | AT&T | plan9 |
---|---|---|---|
操作數順序? | 目標在前,源在后 | 源在前,目標在后 | 源在前,目標在后 |
寄存器前綴 | 無前綴 | 前綴 %,如%eax | 無前綴 |
立即數前綴 | 無前綴 | 前綴 $,如$0x5 | 前綴 $,如$0x5 |
指令后綴? | 無,但通過byte ptr(1字節)、word ptr(2字節)、 dword ptr(4字節)、qword ptr(8字節)修飾符表示多少字節, 如mov qword ptr [rsp+0x10], 0x2 | b(1字節)、w(2字節)、l(4字節)、q(8字節) 如movq $0x2 %rax | 同AT&T,如SUBQ $48, SP |
尋址方式 | []作為修飾符,如[rsp+0x8],定位棧偏移 0x8 處的內存地址 | ()作為修飾符,如8(%ebp),定位棧偏移 0x8 處的內存地址 | 同AT&T |
寄存器抽象 | 不支持 | 不支持 | 支持,也叫偽寄存器,如SB、SP、FP、PC,用于簡化內存尋址 |
函數聲明 | 無專用指令,通過標簽和棧操作實現 | 無專用指令,通過標簽和棧操作實現 | TEXT func(SB), flags, $size |
4.2、nsam匯編
匯編語言程序由三種類型的語句組成:
- 可執行指令:直接告訴處理器要執行的操作,如mov、add等;
- 匯編器指令:指導匯編器如何處理源代碼及其生成的輸出,如section、global _start、db、dw、resb等;
- 宏:在源代碼級別進行文本替換。
- 匯編程序一般分成3個區域:
- data section: 用于聲明初始化的數據或者常量,運行時不會更改;
- bss section: 用于聲明全局變量;
- text section: 用于保存實際的代碼。這個部分必須以聲明global _start開始,它告訴內核程序從哪里開始執行。
這里著重說下section匯編指令。
section 指令用于定義或切換段;
section .my_data progbits alloc write ; 定義可讀寫的初始化數據段
section .my_code progbits alloc exec ; 定義可執行的代碼段
section .my_rodata progbits alloc ; 定義只讀數據段(不可寫)
section .my_data progbits alloc writemy_var: dd 0x12345678 ; 定義可寫數據section .text
global _start
_start:mov eax, [my_var] ; 正常讀取mov dword [my_var], 0 ; 允許寫入
我們一般給程序內存分為代碼段、數據段、堆、棧等、其中代碼段和數據段的內容在匯編中就由section 指令定義:
section .data 存放已初始化全局變量
section .bss 存放未初始化全局變量
section .text 存放可執行代碼,特別注意。這個部分必須以聲明global _start開始,它告訴內核程序從哪里開始執行。
section .rodata 存放只讀數據(如字符串,常量等)
這幾種段無需設置讀寫權限,nsam匯編器會自動追加上,比如還會在.bss后追加nobits alloc write,表示未初始化且可寫,因為一般程序會這么分,約定俗成了。
一個輸出hello world的匯編程序:
#hello.asm 文件
section .datahello db 'Hello, World!',0xA ; 定義字符串并以換行符結束len equ $ - hello ; 計算字符串長度section .textglobal _start ; 指定入口點_start:; 寫字符串到標準輸出mov eax, 4 ; 系統調用號 (sys_write)mov ebx, 1 ; 文件描述符 (stdout)mov ecx, hello ; 要寫入的緩沖區地址mov edx, len ; 緩沖區長度int 0x80 ; 觸發中斷; 退出程序mov eax, 1 ; 系統調用號 (sys_exit)xor ebx, ebx ; 返回狀態碼 0int 0x80 ; 觸發中斷
x86-64下yum安裝下nasm匯編器
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello # Hello, World!
五、參考
1]:維基百科Intel_8086
2]:維基百科x86發展
3]:x86匯編指令
4]:Intel官網SSE
5]:x64 體系結構
6]:nasm官網