子程序功能
它们是可重用的代码段,您的程序可以调用它们来执行各种可重复的任务。子例程使用标签声明,就像我们以前使用的(例如。_start:)然而,我们不使用JMP指令到达它们-相反,我们使用一个新的指令CALL。在运行函数之后,我们也不使用JMP指令返回程序。要从子例程返回到我们的程序,我们使用指令RET代替。
为什么我们不使用JMP来实现子程序呢?
编写子程序的好处是我们可以重用它。如果我们想要在代码的任何地方使用子例程,我们就必须编写一些逻辑来确定我们从代码的哪个位置跳转以及应该跳转到哪个位置。这将使我们的代码充满不必要的标签。然而,如果我们使用CALL和RET,程序集就会使用称为堆栈的东西来处理这个问题。
堆栈简介
堆栈是一种特殊类型的内存。这是我们之前使用过的相同类型的内存,但是它在我们的程序中是特殊的。栈就是所谓的后进先出内存(LIFO)。你可以把它想象成厨房里的一堆盘子。你放的最后一个盘子也是你下次用盘子的时候拿下来的第一个盘子。
堆栈在汇编中不存储板,但它的存储值。您可以在堆栈上存储很多东西,比如变量、地址或其他程序。当调用子例程临时存储稍后将恢复的值时,我们需要使用堆栈。
你的函数需要使用的任何寄存器都应该使用PUSH指令将其当前值放在堆栈上以安全保存。然后,在函数完成其逻辑之后,这些寄存器可以使用POP指令恢复其原始值。这意味着寄存器中的任何值在调用函数之前和之后都是相同的。如果我们在子例程中处理这一点,我们就可以调用函数,而不用担心它们对寄存器做了什么改变。
CALL和RET指令也使用堆栈。当你调用一个子程序时,你在程序中调用它的地址被压入堆栈。然后RET将该地址从堆栈中弹出,程序将跳回代码中的那个位置。这就是为什么你应该总是JMP标签,但你应该调用函数。
; Hello World Program (Subroutines)
; Compile with: nasm -f elf helloworld-len.asm
; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-len.o -o helloworld-len
; Run with: ./helloworld-len
SECTION .data
msg db 'Hello, brave new world!', 0Ah
SECTION .text
global _start
_start:
mov eax, msg ; move the address of our message string into EAX
call strlen ; call our function to calculate the length of the string
mov edx, eax ; our function leaves the result in EAX
mov ecx, msg ; this is all the same as before
mov ebx, 1
mov eax, 4
int 80h
mov ebx, 0
mov eax, 1
int 80h
strlen: ; this is our first function declaration
push ebx ; push the value in EBX onto the stack to preserve it while we use EBX in this function
mov ebx, eax ; move the address in EAX into EBX (Both point to the same segment in memory)
nextchar: ; this is the same as lesson3
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx ; pop the value on the stack back into EBX
ret ; return to where the function was called
~$ nasm -f elf helloworld-len.asm
~$ ld -m elf_i386 helloworld-len.o -o helloworld-len
~$ ./helloworld-len
Hello, brave new world!