汇编语言初识
上一篇文章中初步介绍了汇编语言的编辑器、汇编器与链接器,又让大家尝试了第一个程序。在本篇文章中,我们主要解释一下第一个程序。
# 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
w0
是mov
指令的目的操作数,代表一个寄存器。我们之前提到,在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 $?
查看的结果也会不同。这也是我们初期不用调试器时查看汇编程序结果的一个简单的方法。