说明
- 本学习系列代码几乎完全摘自:asmtutor.com,如果英文可以的(也可以用谷歌浏览器翻译看),可以直接看asmtutor.com上的教程
- 本学习系列目录地址:https://www.cnblogs.com/whuwzp/p/nasm_contents.html
- 系统环境搭建:(我用的是ubuntu18.04.4 server,安装gcc、g++)
sudo apt install nasm
sudo apt install gcc-multilib
0. 概览
- 承前:上一节,我们实现了sprint,但是如果其他asm文件也想用sprint函数怎么办?
- 启后:本节,分割功能,在functions.asm中完成sprint函数,在main.asm中调用它(只需要加
%include functions.asm
),这种方法就可以了。
1. functions.asm: 函数实现
以下代码摘自:https://asmtutor.com/#lesson3
这里实现了slen(计算字符串长度)、sprint(打印)、sprintln(换行打印)、exit(安全退出)
;------------------------------------------
; int slen(String message)
; String length calculation function
slen:
push ebx
mov ebx, eax
nextchar:
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx
ret
;------------------------------------------
; void sprint(String message)
; String printing function
sprint:
push edx
push ecx
push ebx
push eax
call slen
mov edx, eax
pop eax
mov ecx, eax
mov ebx, 1
mov eax, 4
int 80h
pop ebx
pop ecx
pop edx
ret
;------------------------------------------
; void sprintLF(String message)
; String printing with line feed function
sprintLF:
call sprint
push eax ; push eax onto the stack to preserve it while we use the eax register in this function
mov eax, 0Ah ; move 0Ah into eax - 0Ah is the ascii character for a linefeed
push eax ; push the linefeed onto the stack so we can get the address
mov eax, esp ; move the address of the current stack pointer into eax for sprint
call sprint ; call our sprint function
pop eax ; remove our linefeed character from the stack
pop eax ; restore the original value of eax before our function was called
ret ; return to our program
;------------------------------------------
; void exit()
; Exit program and restore resources
quit:
mov ebx, 0
mov eax, 1
int 80h
ret
2. main.asm调用func.asm中的函数
用%include 'functions.asm'
包含,然后就可以直接调用了。
以下摘自:https://asmtutor.com/#lesson7
; Hello World Program (Print with line feed)
; Compile with: nasm -f elf helloworld-lf.asm
; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-lf.o -o helloworld-lf
; Run with: ./helloworld-lf
%include 'functions.asm' ; 关键
SECTION .data
msg1 db 'Hello, brave new world!', 0h ; NOTE we have removed the line feed character 0Ah
msg2 db 'This is how we recycle in NASM.', 0h ; NOTE we have removed the line feed character 0Ah
SECTION .text
global _start
_start:
mov eax, msg1
call sprintLF ; NOTE we are calling our new print with linefeed function
mov eax, msg2
call sprintLF ; NOTE we are calling our new print with linefeed function
call quit
3. 函数参数传递问题和bug记录
3.1 函数参数传递问题
其实这里我们写的函数并不是严格的c中函数:
call sprintLF
:其实sprintlf只是一个代码段的偏移而已,call指令相当于push eip, jmp sprintlf
- 这里可以看出,我们是用eax传参的,但是c编译成程序的是用压栈的方式传参的,当然,这个并不影响功能的实现,只要我们自己约定好也OK,但是如果是在.c中调用.asm中的函数,在参数传递上就会有影响
3.3 bug记录
因为我是看了一下代码后,自己写,所以出现了很多bug,当然也学习类很多,记录一下:
3.3.1 函数最后忘加ret
func1:
xxx1
; 最后没加ret
func2:
xxx2
ret
因为忘了加ret,导致call func1,执行了xxx1,直接执行到了func2 的xxx2了。
这也说明了代码段真的是连续的
3.3.2 push和pop保持堆栈平衡
以sprinlf为例,其中push了两次eax,所以最后也pop了两次,这样堆栈才能平衡,否则ret时执行pop eip, jmp eip
就会跳转到错误的返回地址。
sprintLF:
call sprint
push eax ; push eax onto the stack to preserve it while we use the eax register in this function
mov eax, 0Ah ; move 0Ah into eax - 0Ah is the ascii character for a linefeed
push eax ; push the linefeed onto the stack so we can get the address
mov eax, esp ; move the address of the current stack pointer into eax for sprint
call sprint ; call our sprint function
pop eax ; remove our linefeed character from the stack
pop eax
ret
3.3.3 调用其他函数前用push和pop保存、还原寄存器内容
以sprint为例:sprint要调用slen获取字符串长度,在call slen
前,push了abcd寄存器到栈上保存,调用后又依次pop还原,是因为:slen中修改了ebx的内容,这样当slen返回后ebx的值就不是调用前的值了,可能会对接下来的运行造成影响,所以先都保存一下在栈上,调用后在还原。
slen:
push ebx
mov ebx, eax ;修改了ebx
xxx
ret
sprint:
push edx ;保存
push ecx ;保存
push ebx ;保存
push eax ;保存
call slen
mov edx, eax ; 返回值为字符串长度,保存于eax,赋给edx,后面int 80 syscall会用到
pop eax ;还原
mov ecx, eax
mov ebx, 1
mov eax, 4
int 80h
pop ebx ;还原
pop ecx ;还原
pop edx ;还原
ret
3.3.4 大端小端
以sprintlf为例,这个函数先调用sprint打印要打印的内容,然后又调用sprint打印换行符。
sprintLF:
call sprint
push eax ; 保存eax
mov eax, 0Ah ; 0x0Ah是换行'
'
push eax ; 此时esp内容为0A 00 00 00
mov eax, esp ; 把esp地址给eax
call sprint ; eax 作为参数,打印esp指向的内容
pop eax ; 为了保持堆栈平衡(pop次数等于push次数)
pop eax ; 还原eax
ret
回忆下sprint是如何打印的,它先要找 '获取字符串长度,可以疑问在于:0x0Ah只有一个字符,哪来的' ?'
因为esp的内容是0A 00 00 00,0A后面自然跟了一个00。