系列文章目录
第七章 CALLING FUNCTIONS提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
如何用汇编语言写函数调用,如何传入参数,传出参数,返回数据提示:以下是本篇文章正文内容,下面案例可供参考
一、函数概述
1.1 定义函数
.section .text
.type area, @function
area:
ret
.globl main
main:
call area
老师的这个例子比较简单,我在我的64位CPU的linux虚机上,写了一个函数,编译成汇编。
C语言源程序:
#include <stdio.h>
void* my_memset(void *dst, int val, size_t count)
{
void *start = dst;
while(count--)
{
*(char*)dst = (char) val;
dst = (char*)dst + 1;
}
return start;
}
int main(void)
{
int array[1000];
my_memset((void*)&array[0], 0, sizeof(array));
return 0;
}
gcc -S memset.c编译成汇编文件memset.s:
.file "memset.c"
.text
.globl my_memset # 默认是globl
.type my_memset, @function # 与老师给的例子一样
my_memset:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp) # 用寄存器传递参数,然后压入栈中,-24 = %rbp占8字节 + 返回地址占8字节+%rdi占8字节
movl %esi, -28(%rbp) # 第2个参数占4字节
movq %rdx, -40(%rbp) # 第3个参数占8字节,这里为什么是-40?不是-36?应该是第2个参数8字节对齐后
movq -24(%rbp), %rax
movq %rax, -8(%rbp)
jmp .L2
.L3:
movl -28(%rbp), %eax
movl %eax, %edx
movq -24(%rbp), %rax
movb %dl, (%rax)
addq $1, -24(%rbp)
.L2:
movq -40(%rbp), %rax
leaq -1(%rax), %rdx
movq %rdx, -40(%rbp)
testq %rax, %rax
jne .L3
movq -8(%rbp), %rax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size my_memset, .-my_memset
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $4000, %rsp # 局部变量申请栈内存
leaq -4000(%rbp), %rax # 将栈顶指针赋值给%rax
movl $4000, %edx # memset第3个参数, %edx初始化个数4000
movl $0, %esi # memset第2个参数, %esi初始化值0
movq %rax, %rdi # memset第1个参数,%rdi保存栈顶指针
call my_memset # 与调用C LIB一样
movl $0, %eax # 出栈前,要先将%eax赋值为0,为啥还不知道呢
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
这个例子,参数是用寄存器传输的。my_memset参数没有压栈。
函数操作使用栈内存时,栈顶没有变化。
1.2 指令说明
- push指令
pushl %eax
将eax数值压入栈中,可分解为:
subl $4, %esp ——> esp = esp - 4
movl %eax, (%esp) ——> *(int32_t *)esp = eax - popl指令
pop %eax
将eax数值弹出栈,可分解为:
movl (%esp), %eax ——> eax = *(int32_t *)esp
addl $4, %esp ——> esp = esp + 4 - call指令
call 0x12345
调用0x12345这个地址,可分解为:
pushl %eip ——> 将cpu下一条要执行的指令压入栈中
movl $0x12345, %eip ——> eip = 0x12345
注意:CPU下一条指令将会从地址0x12345中取。 - ret指令
ret
返回call之前的地址,可分解为:
popl %eip ——> 将call压入栈的指令弹出赋给eip
二、传递函数参数
2.1 概述
传递参数/返回值的三种方法:
- 寄存器
- 全局变量
- 栈
寄存器,其中寄存器传参数,大家可以参考前面调用系统提供的接口函数和C LIB的调用。都是用寄存器传递参数的。但是也只是传递这一下,进入函数中如何处理从自己写的函数来看,还是会入栈。但是这里有一个疑问,如果在函数调用前参数都赋值好了,但是此时线程切换了,再切换回来寄存器会不会变化?应该不会,不同线程应该保留各自的寄存器副本。
全局变量,是最不推荐的,会被其他线程修改掉。
栈,栈是必须用的。
2.2 用寄存器传参
2.2.1 一个例子:正方形求面积
movl %eax, %ecx # 一个乘数放在%eax中,另一个乘数放在%ecx中
mul %ecx # 结果放在edx和eax中
例子中没有用到edx,默认认为数字比较小,只处理了eax。只作为一个函数示例,没有很严谨。
在1.1 定义函数 一节中,memset.c编译的memset.s,可以看出使用寄存器传递参数。
2.3 全局变量传参:
例子中有个问题:
end:
movl value, %ebx # 原来写错了movl result1, %ebx
2.4 用栈传参
用栈传递函数参数,需要手工将参数push?到栈中,调用完成之后,需要手工恢复栈顶,即%esp。
栈:
- 从高地址向低地址生长
- 先进后出
- 每个线程都有自己的线程栈
- call 函数后面一个语句的地址压入栈,在函数ret时,会出栈赋值给EIP,执行后续操作
C语言参数入栈:从右至左依次入栈,最右边的参数更靠近栈底,最左边的参数更靠近栈顶。
%esp — 保存栈顶地址
%ebp — 保存栈底地址
调用函数:
将当前%ebp压入栈,并栈底移动到当前栈顶
pushl %ebp
movl %esp, %ebp
参数使用:
老师的例子里都是使用%esp:
- parameter1, 4(%esp)
- parameter2, 8(%esp)
- parameter3, 12(%esp)
但是我用gcc编译汇编,发现都用的是%ebp: - parameter1, -16(%ebp)
- parameter2, -12(%ebp)
- parameter3, -8(%ebp)
- next instruction
不用esp的原因是,%esp随着后续的压栈会变化,所以用%ebp更靠谱。
PPT中movl %esp, %ebp写反了,应该是movl %ebp, %esp
movl %ebp, %esp # 把%esp移动到%ebp指向的栈地址
popl %ebp # 弹出保存的main的栈底赋值给%ebp,同时%esp=%esp+4
ret # ret指令,将%esp指向的返回地址弹出,赋值给rip
因为%esp会在程序使用局部变量时移动,所以不用%esp访问参数,而是用%ebp来访问。
2.5 局部变量压栈
main:
pushl %eax # 参数3压栈
pushl %eax # 参数2压栈
pushl %eax # 参数1压栈
call func1 # 调用func1函数,隐含着将addl %12, %esp指令地址压栈
addl %12, %esp # 栈顶恢复到3个参数压栈前
2.6 一个例子(用栈传参)
这个例子有个问题,就是main函数中没有平栈。另外,返回值还是用寄存器%eax才传递(%edx保存乘法高位,例子程序简化了,没有取%edx的值,默认这个值没有那么大)
pushl $2
call area
# 此处应该平栈,popl %ebx,或者addl $4, %esp # 32位cpu
修改后
.section .bss
.lcomm values, 8
.section .text
.globl main
main:
leal values, %edi
pushl $2
call area
addl $4, %esp # 32位cpu
movl %eax, (%edi) # %edi的括号很关键,表明是赋值给%edi指向的内存,即:values
pushl $3
call area
addl $4, %esp
movl %eax, 4(%edi) # values高地址的4字节
end:
movl (%edi), %ebx # values低地址4字节保存的值赋值给%ebx
addl 4(%edi), %ebx # 高4字节值+ 低4字节值,赋值给%ebx
movl $1, %eax # 指定系统调用为:exit
.int 0x80 # 执行调用:exit(%ebx)
.type area, @function
area:
pushl %ebp # 保存当前%ebp
movl %esp, %ebp # 新的%ebp指向当前栈顶,即保存之前%ebp位置
subl $4, %esp # 栈顶向低地址移动4字节,用于局部变量
movl 8(%ebp), %eax # 8(%ebp)跳过压入栈的%ebp和返回地址,指向$2,即将2赋值给%eax
mull 8(%ebp) # 8(%ebp)*%eax,即2*2,保存在%edx,%eax
movl %eax, -4(%ebp) # 将结果保存在局部变量中,%edx省略了
movl %ebp, %esp # 将栈顶指回area栈底,即保存main,%ebp低栈内存
popl %ebp # %ebp = main:%ebp; addl $4, %esp
ret # popl %eip
在gcc -m32 -o …编译之后,执行报segment fault。