跳轉到內容

x86 彙編/高階語言

來自華夏公益教科書,開放的書籍,開放的世界

很少有專案完全用匯編語言編寫。它通常用於訪問特定於處理器的功能、最佳化程式碼的關鍵部分以及非常低級別的工作,但對於許多應用程式而言,在高階語言(如 C)中實現基本控制流程和資料操作例程可能更簡單更容易。出於這個原因,在組合語言和其他語言之間進行介面通常是必要的。

編譯器

[編輯 | 編輯原始碼]

第一個編譯器只是文字翻譯器,將高階語言轉換為組合語言。然後將組合語言程式碼輸入彙編器,以建立最終的機器程式碼輸出。GCC 編譯器仍然執行此序列(程式碼被編譯成彙編,並饋送到 AS 彙編器)。但是,許多現代編譯器將跳過組合語言並直接建立機器程式碼。

組合語言程式碼的優點是它與底層機器程式碼一一對應。每個機器指令都直接對映到單個彙編指令。因此,即使編譯器直接建立機器程式碼,仍然可以使用匯編語言程式與該程式碼進行介面。重要的是要知道語言如何實現其資料結構、控制結構和函式。高階語言編譯器實現函式呼叫的方法稱為**呼叫約定**。

呼叫約定是函式和函式呼叫者之間的契約,它指定了幾個引數

  1. 引數如何傳遞給函式,以及按什麼順序?它們是壓入堆疊,還是透過暫存器傳遞?
  2. 返回值如何傳遞迴呼叫者?這通常透過暫存器或堆疊來完成。
  3. 哪些處理器狀態是易變的(可供修改)?易變暫存器可供函式修改。呼叫者有責任在需要時儲存這些暫存器的狀態。**非易變**暫存器保證由函式保留。被呼叫函式負責儲存這些暫存器的狀態並在退出時恢復這些暫存器。
  4. 函式**序言**和**結語**,用於設定暫存器和堆疊以供函式內部使用,然後在退出之前恢復堆疊和暫存器。

C 呼叫約定

[編輯 | 編輯原始碼]

對於 C 編譯器,CDECL 呼叫約定是事實上的標準。它因編譯器而異,但程式設計師可以透過在函式宣告之前新增一個關鍵字來指定使用 CDECL 實現函式,例如在 Visual studio 中使用 __cdecl

int __cdecl func()

在 gcc 中,它將是 __attribute__( (__cdecl__ ))

int __attribute__((__cdecl__ )) func()

CDECL 呼叫約定指定了許多不同的要求

  1. 函式引數在堆疊上按**從右到左**的順序傳遞。
  2. 函式結果儲存在 EAX/AX/AL 中
  3. 浮點返回值將返回在 ST0 中
  4. 函式名稱字首為下劃線。
  5. 引數由呼叫者本身從堆疊中彈出。
  6. 8 位和 16 位整數引數被提升為 32 位引數。
  7. 易變暫存器是:EAX、ECX、EDX、ST0 - ST7、ES 和 GS
  8. 非易變暫存器是:EBX、EBP、ESP、EDI、ESI、CS 和 DS
  9. 函式將使用 RET 指令退出。
  10. 該函式應該透過 EAX/AX 中的引用返回類或結構的值型別。該空間應該由函式分配,該函式無法使用堆疊或堆,只能在靜態非常量儲存中留下固定地址。這本質上不是執行緒安全的。許多編譯器會破壞呼叫約定
    1. GCC 使呼叫程式碼分配空間並透過堆疊上的隱藏引數傳遞指向該空間的指標。被呼叫函式將返回值寫入該地址。
    2. Visual C++ 將
      1. 將 32 位或更小的 POD 返回值傳遞到 EAX 暫存器。
      2. 將大小為 33-64 位的 POD 返回值透過 EAX:EDX 暫存器傳遞
      3. 對於非 POD 返回值或大於 64 位的值,呼叫程式碼將分配空間並透過堆疊上的隱藏引數傳遞指向該空間的指標。被呼叫函式將返回值寫入該地址。


CDECL 函式能夠接受可變引數列表。以下是如何使用 cdecl 呼叫約定的示例

global main

extern printf

section .data
	align 4
	a:	dd 1
	b:	dd 2
	c:	dd 3
	fmtStr:	db "Result: %d", 0x0A, 0

section .bss
	align 4

section .text
				
;
; int func( int a, int b, int c )
; {
;	return a + b + c ;
; }
;
func:
	push	ebp		; Save ebp on the stack
	mov	ebp, esp	; Replace ebp with esp since we will be using
				; ebp as the base pointer for the functions
				; stack.
				;
				; The arguments start at ebp+8 since calling the
				; the function places eip on the stack and the
				; function places ebp on the stack as part of
				; the preamble.
				;
	mov	eax, [ebp+8]	; mov a int eax
	mov	edx, [ebp+12]	; add b to eax
	lea	eax, [eax+edx]	; Using lea for arithmetic adding a + b into eax
	add	eax, [ebp+16]	; add c to eax
	pop	ebp		; restore ebp
	ret			; Returning, eax contains result

	;
	; Using main since we are using gcc to link
	;
	main:

	;
	; Set up for call to func(int a, int b, int c)
	;
	; Push variables in right to left order
	;
	push	dword [c]
	push	dword [b]
	push	dword [a]
	call	func
	add	esp, 12		; Pop stack 3 times 4 bytes
	push	eax
	push	dword fmtStr
	call	printf
	add	esp, 8		; Pop stack 2 times 4 bytes

	;
	; Alternative to using push for function call setup, this is the method
	; used by gcc
	;
	sub	esp, 12		; Create space on stack for three 4 byte variables
	mov	ecx, [b]
	mov	eax, [a]
	mov	[esp+8], dword 4
	mov	[esp+4], ecx
	mov	[esp],	 eax
	call	func
	;push	eax
	;push	dword fmtStr
	mov	[esp+4], eax
	lea	eax, [fmtStr]
	mov	[esp], eax
	call	printf

				;
				; Call exit(3) syscall
				;	void exit(int status)
				;
	mov	ebx, 0		; Arg one: the status
	mov	eax, 1		; Syscall number:
	int 	0x80

為了組裝、連結和執行程式,我們需要執行以下操作

nasm -felf32 -g cdecl.asm
gcc -o cdecl cdecl.o
./cdecl

STDCALL 是在 Microsoft Windows 系統上與 Win32 API 互動時使用的呼叫約定。STDCALL 由 Microsoft 建立,因此並不總是受非 Microsoft 編譯器支援。它因編譯器而異,但程式設計師可以透過在函式宣告之前新增一個關鍵字來指定使用 STDCALL 實現函式,例如在 Visual studio 中使用 __stdcall

int __stdcall func()

在 gcc 中,它將是 __attribute__( (__stdcall__ ))

int __attribute__((__stdcall__ )) func()

STDCALL 具有以下要求

  1. 函式引數在堆疊上按**從右到左**的順序傳遞。
  2. 函式結果儲存在 EAX/AX/AL 中
  3. 浮點返回值將返回在 ST0 中
  4. 64 位整數和 32/16 位指標將透過 EAX:EDX 暫存器返回。
  5. 8 位和 16 位整數引數被提升為 32 位引數。
  6. 函式名稱字首為下劃線
  7. 函式名稱字尾為 "@" 符號,後跟傳遞給它的引數的位元組數。
  8. 引數由被呼叫者(被呼叫函式)從堆疊中彈出。
  9. 易變暫存器是:EAX、ECX、EDX 和 ST0 - ST7
  10. 非易變暫存器是:EBX、EBP、ESP、EDI、ESI、CS、DS、ES、FS 和 GS
  11. 函式將使用 RET n 指令退出,被呼叫函式在返回時將從堆疊中彈出另外 n 個位元組。
  12. 32 位或更小的 POD 返回值將返回到 EAX 暫存器。
  13. 大小為 33-64 位的 POD 返回值將透過 EAX:EDX 暫存器返回。
  14. 對於非 POD 返回值或大於 64 位的值,呼叫程式碼將分配空間並透過堆疊上的隱藏引數傳遞指向該空間的指標。被呼叫函式將返回值寫入該地址。

STDCALL 函式無法接受可變引數列表。

例如,以下 C 語言中的函式宣告

_stdcall void MyFunction(int, int, short);

將在彙編中使用以下函式標籤訪問

_MyFunction@12

請記住,在 32 位機器上,在堆疊上傳遞 16 位引數(C “short”)佔用了 32 位的空間。

FASTCALL 函式通常可以使用許多編譯器中的 __fastcall 關鍵字來指定。FASTCALL 函式將前兩個引數傳遞給函式中的暫存器,這樣就可以避免耗時的堆疊操作。FASTCALL 具有以下要求

  1. 第一個 32 位(或更小)引數在 ECX/CX/CL 中傳遞(參見 [1]
  2. 第二個 32 位(或更小)引數在 EDX/DX/DL 中傳遞
  3. 其餘函式引數(如果有)按從右到左的順序在堆疊上傳遞
  4. 函式結果在 EAX/AX/AL 中返回
  5. 函式名稱以 "@" 符號為字首
  6. 函式名稱以 "@" 符號為字尾,後跟以位元組為單位傳遞的引數大小。

C++ 呼叫約定(THISCALL)

[edit | edit source]

C++ THISCALL 呼叫約定是 C++ 的標準呼叫約定。在 THISCALL 中,函式的呼叫方式與 CDECL 約定幾乎相同,但必須傳遞 **this** 指標(指向當前類的指標)。

**this** 指標的傳遞方式取決於編譯器。Microsoft Visual C++ 將其傳遞到 ECX 中。GCC 將其傳遞,就好像它是函式的第一個引數一樣。(即在返回地址和第一個形式引數之間。)

Ada 呼叫約定

[edit | edit source]

Pascal 呼叫約定

[edit | edit source]

Pascal 約定本質上與 cdecl 相同,唯一的區別是

  1. 引數按從左到右的順序壓入(邏輯上的西方世界閱讀順序)
  2. 被呼叫的例程必須在返回之前清理堆疊

此外,32 位堆疊上的每個引數必須使用 DWORD 的所有四個位元組,而不管資料的實際大小如何。

這是 Windows API 例程使用的主要呼叫方法,因為它在記憶體使用、堆疊訪問和呼叫速度方面略微高效。


注意:Pascal 約定與 Borland Pascal 約定不同,Borland Pascal 約定是 fastcall 的一種形式,使用暫存器(eax、edx、ecx)傳遞前三個引數,也稱為暫存器約定。

Fortran 呼叫約定

[edit | edit source]

內聯彙編

[edit | edit source]

C/C++

[edit | edit source]

這個 Borland C++ 例子將 `byte_data` 分割成 `buf` 中的兩個位元組,第一個位元組包含高 4 位,低 4 位在第二個位元組中。

void ByteToHalfByte(BYTE *buf, int pos, BYTE byte_data)
{
  asm
  {
    mov al, byte_data
    mov ah, al
    shr al, 04h
    and ah, 0Fh
    mov ecx, buf
    mov edx, pos
    mov [ecx+edx], al
    mov [ecx+edx+1], ah
  }
}

Pascal

[edit | edit source]

FreePascal 編譯器 (FPC) 和 GNU Pascal 編譯器 (GPC) 允許使用 `asm` 塊。雖然 GPC 只接受 AT&T 語法,但 FPC 可以同時使用兩種語法,並允許直接傳遞給彙編器。以下兩個示例是用 FPC 編寫的(關於編譯器指令)。

program asmDemo(input, output, stderr);

// The $asmMode directive informs the compiler
// which syntax is used in asm-blocks.
// Alternatives are 'att' (AT&T syntax) and 'direct'.
{$asmMode intel}

var
	n, m: longint;
begin
	n := 42;
	m := -7;
	writeLn('n = ', n, '; m = ', m);
	
	// instead of declaring another temporary variable
	// and writing "tmp := n; n := m; m := tmp;":
	asm
		mov rax, n // rax := n
		// xchg can only operate at most on one memory address
		xchg rax, m // swaps values in rax and at m
		mov n, rax // n := rax (holding the former m value)
	// an array of strings after the asm-block closing 'end'
	// tells the compiler which registers have changed
	// (you don't wanna mess with the compiler's notion
	// which registers mean what)
	end ['rax'];
	
	writeLn('n = ', n, '; m = ', m);
end.

在 FreePascal 中,你也可以用匯編語言編寫整個函式。另外要注意,如果你使用標籤,你必須事先宣告它們(FPC 要求)。

// the 'assembler' modifier allows us
// to implement the whole function in assembly language
function iterativeSquare(const n: longint): qword; assembler;
// you have to familiarize the compiler with symbols
// which are meant to be jump targets
{$goto on}
label
	iterativeSquare_iterate, iterativeSquare_done;
// note, the 'asm'-keyword instead of 'begin'
{$asmMode intel}
asm
	// ecx is used as counter by loop instruction
	mov ecx, n // ecx := n
	mov rax, 0 // rax := 0
	mov r8, 1 // r8 := 1
	
	cmp ecx, rax // ecx = rax [n = 0]
	je iterativeSquare_done // n = 0
	
	// ensure ecx is positive
	// so we'll run against zero while decrementing
	jg iterativeSquare_iterate // if n > 0 then goto iterate
	neg ecx // ecx := ecx * -1
	
	// n^2 = sum over first abs(n) odd integers
iterativeSquare_iterate:
	add rax, r8 // rax := rax + r8
	inc r8 // inc(r8) twice
	inc r8 // to get next odd integer
	loop iterativeSquare_iterate // dec(ecx)
	// if ecx <> 0 then goto iterate
	
iterativeSquare_done:
	// the @result macro represents the functions return value
	mov @result, rax // result := rax
// note, a list of modified registers (here ['rax', 'ecx', 'r8'])
//    is ignored for pure assembler routines
end;

進一步閱讀

[edit | edit source]

有關高階程式設計結構如何轉換為組合語言的深入討論,請參見 逆向工程

華夏公益教科書