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上的汇编入门(七)——字面量与局部变量