跳转到内容

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]

有关高级编程结构如何转换为汇编语言的深入讨论,请参见 逆向工程

华夏公益教科书