英文原版地址:NASM Assembly Language Tutorials - asmtutor.com
介绍子程序
子程序是函数,是可复用的一段代码,能被你的程序调用去执行各种可重复的任务。子程序使用标签声明,就像之前的章节中使用一样,然而我们不用JMP指令去到达它们,而是使用CALL指令代替。在运行函数后我们也不用JMP指令去返回我们的程序。从子程序返回我们使用RET指令代替。
为什么我们不用JMP指令进入子程序?
这对于我们编写能重复使用的子程序是好事。如果我们想在代码的任何地方使用子程序,我们不得不写一些逻辑去决定在代码中从哪开始跳转以及跳转回哪里,我们的代码将被不想要的标签搞乱。但是,如果我们使用CALL和RET,汇编使用被称作栈的东西来帮助我们解决这个问题。
介绍下栈
栈是一种特殊类型的内存。它和我们之前使用的内存类型相同,然而它特殊在被我们的程序使用。栈时后进先出的内存。你可以认为栈像厨房中的一叠盘子。放上去的最后一个盘子也是下次你拿起用的第一个盘子。
不过在汇编中的栈不是存盘子的,是存值的。你可以在栈中存很多东西,例如变量,地址或其他程序。当我们调用子程序时我们需要使用栈来暂存之后要复原的值。
为了安全,函数需要使用的任何寄存器应该使用PUSH指令将它当前的值放在栈上。在函数已经完成它的逻辑之后,这些寄存器可以用POP指令恢复它们的初始值。这意味着任何寄存器中的值在调用函数前后都将保持不变。如果你在我们的子程序中注意到这一点,我们可以调用函数而不用担心它们对我们的寄存器做什么修改。
CALL和RET指令也使用栈。当你调用一个子程序。你调用地址将从你的程序压入栈中。这个地址时RET指令从栈中弹出的,是这个程序跳转返回到你的代码的位置。这就是为什么你总是跳转到标签但是你应该用CALL调用函数。
使用下边的命令编译,链接然后执行这个程序 hello.asm
SECTION .data
msg db 'Hello, brave new world!', 0Ah ; 指定msg变量为你的消息字符串
SECTION .text
global _start
_start:
mov eax, msg ; 将字符串的地址存到EAX
call strlen ; 调用函数计算字符串长度
mov edx, eax ; 要写入的字节数,字符数加0ah
mov ecx, msg ; 将消息的内存地址复制ECX寄存器
mov ebx, 1 ; 写到标准输出
mov eax, 4 ; 调用 SYS_WRITE (kernel opcode 4)
int 80h
mov ebx, 0 ; 返回状态值0退出,无错误
mov eax, 1 ; 调用 SYS_EXIT (kernel opcode 1)
int 80h
strlen: ; 这是我们声明的第一个函数
push ebx ; 将EBX的值压入栈中保留,在这个函数中我们要使用EBX
mov ebx, eax ; 将EAX中的地址传给EBX (都指向内存中相同的位置)
nextchar:
cmp byte [eax], 0 ; 将EAX指向的字节与0做比较 (0是字符串结束的分隔符)
jz finished ; 跳转 (如果ZF标记被设置) 跳转到标签'finished'指向的代码
inc eax ; EAX中的地址增加1字节 (如果ZF标记没有被设置)
jmp nextchar ; 跳转到标签'nextchar'指向的代码
finished:
sub eax, ebx ; 将EAX中的地址减去EBX中的地址
pop ebx ; 将栈中的值弹出到EBX中
ret ; 返回函数被调用的地址
编译命令
nasm -f elf hello.asm
链接命令
ld -m elf_i386 hello.o -o hello
执行命令
./hello