Skip to content

Latest commit

 

History

History
126 lines (77 loc) · 8.27 KB

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

File metadata and controls

126 lines (77 loc) · 8.27 KB

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

# 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, lq, 分别表示操作的是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上的汇编入门(七)——字面量与局部变量