Evian Zhang's
naive blog

macOS上的汇编入门(六)——汇编语言初识

上一篇文章中初步介绍了汇编语言的编辑器、汇编器与链接器,又让大家尝试了第一个程序。在本篇文章中,我们主要解释一下第一个程序。

# exit.s    
    .section    __TEXT, __text
    .globl  _main
_main:
    movq    $0, %rax
    retq

注释

程序的第一行是注释。在macOS的 as 汇编器语法下,注释由 # 开头,在进行汇编的时候会自动将其处理为空白字符。

我们习惯上将注释写在语句的上方(如例程)或后方,如:

movq	$0, %rax	# mov 0 to register rax

缩进

在最古老的机器上,汇编代码的文本包含四列:标签、助记符、操作数与注释。汇编器通过识别一个文本在哪个列来判断该文本有什么作用。现代的汇编器已经抛弃了这种方法,采用先进的词法分析技术来判断。但是,我们最好仍然按照这种格式来缩进。

汇编器指令(Directive)

"Directive"是汇编语言中一个重要的组成部分,然而它的中文译名似乎还不固定,这里暂且叫它汇编器指令。在汇编语言中,以 . 开头的都是汇编器指令,如例程中的 .section , .globl 等。由汇编器指令开头的语句,一般不会被直接翻译成机器码。汇编器指令并不是告诉汇编器 做什么 , 而是告诉汇编器 如何做 。就比如说例程中, movq $0, %rax 会被汇编器直接翻译为机器码,最终会由CPU直接执行,而 .section __TEXT,__text , 则不会被翻译成机器码,在最终的可执行文件中也不会找到这句话的踪影。它的作用是告诉汇编器如何汇编。下面,就介绍一下 .section 的作用

.section

我们之前在 操作系统基础 中提到,mach-o可执行文件的Data部分拥有许多段(Segment), 每个段又有许多节(section). 同一个段的作用往往是类似的,同时在执行的时候一个段会被分配到一个页之中。而 .section 最常用的格式,就是

.section	segname, sectname

其中 segment 是段名, sectname 是节名。我们目前编写的第一个汇编语言程序,只包含纯代码。在macho中,纯代码被放在了 __TEXT 段的 __text 节中,因此,我们在文件的第二行写了

.section	__TEXT, __text

代表之后的语句都是 __TEXT 段的 __text 节中。

此外,由于这个节过于常用,因此,汇编器给予了我们一个简单的记号: .text . 我们可以直接用 .text 代替 .section __TEXT, __text . 在以后的程序中,我也都会用这种记号。

除了 __TEXT __text 节后,还有许多段和节。常用的段和节的名称和作用可参见 Assembler Directives . 我们之后更复杂的程序中也会用到更多的段和节。

.globl

我们在由汇编语言翻译机器码的时候,得到的文件并不仅仅包含操作的指令,还需要包含一些名字和记号。比如说,C语言中,程序执行的起点是 main 函数。那么,这个函数的名字 main 就要包含在文件中,使得程序执行的时候知道执行哪个函数。

_main

macOS中,汇编语言程序执行的起点是 _main 函数。关于函数与下一行的 _main: 标签,我会在之后的文章中提到。是谁决定它叫这个名字的呢,是链接器。如果我们写的程序想把它主函数叫做 _start , 那么只需要在链接的时候写上

ld -e _start exit.o -o exit -lSystem

即可。

movq

movq 是我们遇到的第一个真正的指令。在汇编语言中,这种能直接翻译成机器码的指令被称作助记符(mnemonic). 之前我们也提到过,在GAS语法下,一条指令是助记符+源+目的,也就是说,它后面紧跟的是源操作数,然后是目的操作数。在x86-64架构下所有的可以被识别的助记符可以参考 64-ia-32-architectures-software-developer-instruction-set-reference-manual , 但值得注意的是,这份官方的参考文档是用的Intel语法,我们只需要把源和目的颠倒过来看就行。

首先我们先要理解 mov . 这是一个在汇编语言中很常见的指令,意思是赋值。 mov a b 就是将 a 赋值给 b . 它可以将立即数赋值给寄存器、内存,可以把寄存器赋值给寄存器、内存,把内存赋值给寄存器。

接下来,我们需要理解 q . 我们思考一下一个场景:我们在C语言中用 long a; 在一块内存上存储了一个64位整型数 a ,又用 int b; 在一块内存上存储了一个32位整型数 b 。那么,每次我们给 a 赋值的时候,实质上都是将数放入 a 的地址对应的内存中。因此,就是一个 mov 指令。但是,如果只有 mov 指令的话,那么 a = 0x114514; b = 0x114514; 这两个C语句翻译成汇编语言的话并没有区别,都是将一个数赋值给一块内存地址。然而我们知道,在x86-64架构下采用小端法,因此,在 a 的内存区域中实际应该存储的是 14 45 11 00 00 00 00 00 , b 的内存区域中存储的是 14 45 11 00 . 这看上去似乎没有什么区别。然而,在向 a 赋值的时候,实际上是把整个8个字节的高位都清零,而 b 仅仅是把4个字节的高位清零。然而,汇编层面并不认得 long , int 的变量之类,因此,就必须扩展助记符来完成这个事情。

在GAS语法中,会在助记符后加上 b , w , l q , 分别表示操作的是1个,2个,4个或8个字节。因此, long 的赋值可以用 movq , int 的赋值可以用 movl .

$0

接着 movq 的,是 $0 , 作为其源操作数。在GAS语法下,一个数字前加上 $ 表示这个数本身。如果不加的话,则表示 0 这个地址里存储的数。此外,我们也可以在前面加 0x 来表示16进制数,如

movq	$0x2000001, %rax

%rax

我们之前提到,在x86-64架构下,CPU中一共有16个64位通用寄存器,它们的名字依次是rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, r8, r9, r10, r11, r12, r13, r14, r15. 当我们用这些名字的时候,指的就是这16个64位通用寄存器。此外,对于前8个通用寄存器,也就是名字不是数字的寄存器,还可以用eax, ebx, ecx, edx, edi, esi, ebp, esp指代其低32位,用ax, bx, cx, dx, di, si, bp, sp指代其低16位。而对于rax, rbx, rcx, rdx这四个通用寄存器而言,还可以单独引用它低16位中的高8位和低8位,如对ax而言,ah指代其高8位,al指代其低8位。

在GAS语法中,寄存器名字前面一定要跟着 % .

retq

关于这个,我会在之后的函数部分的文章中提到。

总结

因此,根据以上的讨论,我们可以将第一个汇编程序翻译成C程序了:

// exit.c
int main()
{
	return 0;
}

这就是我们第一个汇编程序的作用,也就是将 main 函数返回 0 . 至于为什么要将 0 传入rax寄存器而不是别的寄存器,后面关于调用约定的文章中会提及。在终端下,我们可以先运行这个程序 exit :

./exit

什么都没出现,它正确退出了。接着,我们可以用

echo $?

来查看上一个程序的返回结果。不出所料,它返回的是 0 .

我们也可以通过修改第一个汇编程序,将不同的数赋值给rax寄存器,那么,最终 main 函数返回的值也会不同,我们通过 echo $? 查看的结果也会不同。这也是我们初期不用调试器时查看汇编程序结果的一个简单的方法。

可以在哪看到这系列文章

我在我的 GitHub 上, 知乎专栏 上和 CSDN 上同步更新。

上一篇文章: macOS上的汇编入门(五)——第一个汇编程序

下一篇文章: macOS上的汇编入门(七)——字面量与局部变量