高级语言教程从Hello world程序开始是惯例,但汇编语言不太一样,Hello world程序也需要更多知识才能写出,通常在整个教程的三分之一之后才会讲。这个教程我会把Hello world程序放在比较靠前的位置,作为第二个汇编程序。第一个程序在上一篇教程出现,运行后没有任何输出。我们现在回顾一下:
global _main
_main:
mov rax, 0
ret
我会先讲一些预备知识,再解释这段程序,再之后开始我们的Hello world程序。
预备知识——C和汇编程序的产生过程
生成过程
一个C语言源程序文件,需要经过两步才能转换成可执行文件。
- 使用C编译器对源文件进行编译,生成“目标文件”
- 使用“链接器”对一个或多个目标文件进行链接,生成可执行文件
汇编语言同样是两个步骤:
- 使用汇编器对源文件进行汇编,生成“目标文件”
- 使用“链接器”对一个或多个目标文件进行链接,生成可执行文件
除了第一步两处斜体字不同外,都是一样的。其实过程是相同的,只是术语不同,“编译”是针对高级语言的,所用的工具是“编译器”,“汇编”是针对汇编语言的,使用的工具是“汇编器”,我们使用的nasm是一种汇编器。事实上,你按高级语言的叫法把“汇编”和“汇编器”叫做“编译”和“编译器”也没什么不妥,大家也能听得懂。
C程序生成
我们开始实战C语言的程序生成,我们有C语言的Hello world程hello.c,如下
#include <stdio.h>
int main() {
printf("Hello world\n");
return 0;
}
第一步编译:
在终端输入命令
gcc -c hello.c
便会在当前目录下生成目标文件hello.o
。
第二步:链接
我们接着输入命令
gcc hello.o
便会在当前目录下生成可执行文件a.out
。
输入
./a.out
就可以看到显示出了Hello world,默认的可执行文件名a.out有点奇怪,我们可加上-o
参数,指定生成的文件名
gcc hello.o -o hello
就生成了名为hello的可执行文件。
我们的编译器和链接器都是gcc,这种简单的程序通常我们把两步合成一步
gcc hello.c -o hello
这样会生成可执行文件hello,不会生成目标文件hello.o
。
汇编程序生成
之前写好的hello.s
文件
第一步:汇编
nasm -f macho64 test.s
这个命令会生成目标文件test.o
-f macho64
表示生成macOS平台x86_64格式的目标文件。如果你用-f macho
会生成32位的目标文件,虽然可以完成汇编,但你在链接的时候会出错,因为macOS只支持64位程序, 无法链接32位的目标文件,这也是系统自带的nasm汇编器不能用的原因。
第二步:链接
gcc test.o -o test
这就会生成可执行文件test。
这里的工具仍是gcc,命令看起来也一样,看起和C语言的链接过程什么区别。是的,没有区别,我们用的是一个工具。
gcc在对目标文件链接的时候,并不知道目标文件是什么语言生成的,可能是汇编,也可能是C、Go、D等高级语言。通过这种方式可以很容易混合汇编和高级语言编程。后边的教程就会有汇编和C一起工作的例子。
执行程序
我们的test程序执行后虽然看不到输出,但是这个程序是有返回值的。
我们执行
./test
后,接着输入命令
echo $?
我们看到显示0,$?
表示上一条命令的返回值,在类Unix系统,程序返回0表示成功,1到255表示程序失败。
你也可两条命令写在一行
./test ; echo $?
分号是命令分隔符。
第一个汇编程序解释
global _main
表示程序入口是_main:
处,你也可以把_main
修改成别的名字,改后程序链接的时候要指明入口,要麻烦一些,最好不要改。
mov rax, 0
表示把0放入rax寄存器,寄存器你可以简单理解为在CPU内部的超高速内存。CPU有多个寄存器,rax是一个寄存器的名称,下一篇教程会讲。
ret
表示返回
你可以试着把程序里的0改成1或者260,重新运行一下,并查看$?
的值,看看是什么结果。
回顾一下我们学过的两条指令
mov
指令,把数据放入寄存器中
ret
指令,返回
Hello world程序
开始汇编语言的Hello world之前,先写一个C语言的Hello world,之后再转化成汇编,hello2.c如下:
#include <unistd.h>
int main() {
char *msg = "Hello world\n"; // 定义要输出的文字msg
write(1, msg, 12); // 输出msg,12为msg的长度
_exit(0); // 调用_exit函数返回
}
等等,之前不是写过了吗,为啥又写一个?之前的hello.c转化成汇编有点麻烦,所以要另写一个。
写一个等价的汇编程序hello1.s
msg: db "Hello World", 0x0a
global _main
_main:
mov rax, 0x2000004
mov rdi, 1
mov rsi, msg
mov rdx, 12
syscall
ret
mov rax, 0x2000001
mov rdi, 0
syscall
ret
汇编并链接
nasm -f macho64 hello1.s && gcc hello1.o -o hello1
&&
的作用是连接多条命令,但某一条命令失败(返回值不为0),就不再执行后面的命令。和之前提到的分号(;
)不同,分号不管成功与否都会依次执行命令。
会出现警告:
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not allowed in code signed PIE, but used in _main from hello1.o. To fix this warning, don't compile with -mdynamic-no-pic or link with -Wl,-no_pie
先不管,接着运行./hello1
可以看到能正常输出Hello world。
程序的解释我先以注释形式放在程序内,汇编的注释以分号开头,到行末结束。
; 定义要输出的文字msg,db是data byte的意思
; 0x0a表示换行符,0x前缀表示十六进制,
; 也可以用h后缀表示十六进制,比如41h,0ch,以a~f开头的十六进制前面一定要加0
msg: db "Hello World", 0x0a
global _main
_main:
; 要调用的write函数,放入寄存器rax
mov rax, 0x2000004
mov rdi, 1 ; 第1个参数1,放入寄存器rdi
mov rsi, msg ; 第2个参数msg,放入寄存器rsi,此行链接时会报警告
mov rdx, 12 ; 第3个参数12,放入寄存器rdx
syscall ; 调用rax寄存器中的函数
ret ; 函数调用返回
; 要调用的_exit函数,放入寄存器rax
mov rax, 0x2000001
mov rdi, 0 ; 第1个参数0,放入寄存器rdi
syscall ; 调用rax寄存器中的函数
ret ; 函数调用返回
从注释中可以看到
要调用的函数需要放入寄存器rax
中,参数要依次放入寄存器rdi
,rsi
,rdx
中。
我们修复一下警告,并重构一下代码
警告是由指令mov rsi, msg
引起的。
这条指令的意思是把msg
的地址放到寄存器rsi
中,而链接器认为你使用了绝对地址,不能直接使用msg的地址。
我们用lea rsi [rel msg]
替换刚才的语句就可以了。
12是Hello World字符串的长度,改了字符串还要改这个值,可以自动计算字符串的长度
两段函数调用处都有,syscall
和ret
语句,这两句是调用系统内核,可以提取一个公用的代码段
修复了这三个问题的程序hello2.s如下:
SECTION .data ; 数据代码段
msg: db "Hello World", 0x0a
len: equ $-msg ; 计算msg的长度,赋值给len
SECTION .text ; 程序代码段
global _main
kernal:
syscall
ret
_main:
mov rax, 0x2000004
mov rdi, 1
lea rsi, [rel msg]
mov rdx, len ; 把len的值作为参数传入
call kernal ; 调用kernal处的代码
mov rax, 0x2000001
mov rdi, 0
call kernal
这段hello2.s程序修复了上面所说的3个问题,并添加了SECTION .data
和SECTION .text
代码段说明,使程序看起来更明了,.data
和.text
代码段的名称是汇编器定义的,不能更改。
好了,运行正常,格式规范的Hello world程序1.0版就完成了。汇编语言的Hello world这么复杂!你一定有些不明白的地方吧,比如lea
和mov
指令有什么区别,rax中存放的数据是什么意思?随着教程的继续,这些问题会逐渐明朗起来的。