汇编语言初识

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

# 5-basic.s
    .section    __TEXT,__text
    .globl  _main
    .p2align    2
_main:
    mov    w0, #0
    ret

注释

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

我们习惯上将注释写在语句的上方(如例程)或后方。在语句后方写注释时,一般采用;作为注释开头的符号,如:

mov    w0, #0    ; Mov 0 to register w0

缩进

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

也就是说,我们在写一个完整程序的时候,一般会将指令缩进4个空格,而如_main:之类的标签则不进行缩进。

汇编器指令(Directive)

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

逐行分析第一个汇编程序

.section

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

.section    segname, sectname

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

.section    __TEXT, __text

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

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

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

.globl

在一个程序编译、链接、动态链接的过程中,有一些变量、函数的名字,需要作为字符串存储在二进制程序中,以便将来的某些时候使用。因此,我们可以指定一些标识符的可见性(Visibility)。

对于这个程序而言,我们在学习C语言的时候就了解到,main函数是一个C语言程序开始的起点。事实上,链接器需要知道main函数这个名字,以便后续与C运行时的链接。因此,我们可以用.globl _main的方式,让链接器知道我们提供了main函数。

对于符号、可见性、链接等概念,之后会详细介绍。

_main

macOS中,C语言程序执行的起点在汇编层面是_main函数。关于函数与之后的_main:标签,我会在之后的文章中提到。

.p2align

.section.globl一样,这也是一个汇编器指令。这个汇编器指令的作用是指令对齐。关于这一点,也会在之后的文章中提到。

mov

mov是我们遇到的第一个真正的指令。在汇编语言中,这种能直接翻译成机器码的指令被称作助记符(mnemonic)。在GNU语法下,一条指令可以粗略地看作是助记符+目的+源,也就是说,它后面紧跟的是目的操作数,然后是源操作数。

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

w0

w0mov指令的目的操作数,代表一个寄存器。我们之前提到,在AArch64架构下,CPU中一共有31个64位通用寄存器。关于这点后面的文章中会介绍。

#0

mov的源操作数是#0。一般来说,在汇编语言中的常数都会在前加#符号,让读者看得更清楚。当然,不加这个#一样可以正常进行汇编。

此外,我们也可以在前面加0x来表示16进制数,如

mov    w0, #0xFF

ret

这个指令可以类似于C语言中的return。关于这个,我会在之后的函数部分的文章中提到。

总结

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

// 5-basic.c
int main() {
    return 0;
}

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

./5-basic

什么都没出现,它正确退出了。

接着,我们可以用

echo $?

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

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