NASM汇编学习系列(3)——多汇编文件间函数调用

说明

  1. 本学习系列代码几乎完全摘自:asmtutor.com,如果英文可以的(也可以用谷歌浏览器翻译看),可以直接看asmtutor.com上的教程
  2. 本学习系列目录地址:https://www.cnblogs.com/whuwzp/p/nasm_contents.html
  3. 系统环境搭建:(我用的是ubuntu18.04.4 server,安装gcc、g++)
sudo apt install nasm
sudo apt install gcc-multilib

0. 概览

  1. 承前:上一节,我们实现了sprint,但是如果其他asm文件也想用sprint函数怎么办?
  2. 启后:本节,分割功能,在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中函数:

  1. call sprintLF:其实sprintlf只是一个代码段的偏移而已,call指令相当于push eip, jmp sprintlf
  2. 这里可以看出,我们是用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。

原文地址:https://www.cnblogs.com/whuwzp/p/nasm_multifiles.html