【Linux x86汇编踩坑】函数调用过程 函数栈浅析
2018-01-18 12:21
330 查看
【Linux x86汇编踩坑】函数调用过程 函数栈浅析
前言
汇编也和其他高级语言一样,都有函数的概念,倒不如说是某些高级语言的函数调用的底层实现,总的来说,使用函数可以把功能细分,还可以划分模块,最重要的是减少重复代码,使程序更加容易维护。汇编的函数跟高级的语言的函数有些不一样,普通高级语言只需要定义函数,函数声明,调用函数就行了,有些语言还可以直接省略了函数声明的步骤,都由编译器或解释器来完成了。但是汇编就不一样,它需要自己来做被编译器或解释器省略掉的事情。编写一个计算幂的函数
伪代码表示:func power(param1,param2){return param1^param2}
用汇编写大概是这样的:(这里先上代码具体步骤后面再讲)
.section .data .section .text .globl _start _start: #计算2的三次方的值 #压入参数列表 pushl $3 pushl $2 call power #栈指针归位 addl $8,%esp movl %eax,%ebx movl $1,%eax int $0x80 #power 函数 #用于计算幂 .type power, @function power: pushl %ebp movl %esp, %ebp #保存参数 movl 8(%ebp),%eax movl 12(%ebp),%ecx #开始计算 start_loop: cmpl $1,%ecx je loop_exit addl %eax,%eax decl %ecx jmp start_loop loop_exit: popl %ebp ret
这是一个计算2的三次方的函数
汇编
as power.s -o power.o
连接
ld power.o -o power
运行
./power echo $? //8
函数的调用过程
可以看到成功打印出了8,但是函数调用究竟是怎样的过程呢?首先函数调用需要用到一个栈,这个栈也可以叫做函数栈,栈是一个先进后出的数据结构整个栈的地址是由栈顶向栈底地址逐渐减小的,一个栈形如:
一个函数调用时需要用到的栈称为函数栈,函数栈并没有什么两样,在栈中,存放函数需要用到的参数列表,局部变量等,同时还要存放函数的返回地址
在主程序中,调用函数时,要做两件事情,第一是把eip(指令指针)指向函数的首地址,第二是保存下一条指令,把它作为函数的返回地址入栈,幸运的是这两步都不需要我们去做。结合上面的计算幂的例子,该函数需要用到两个参数,一个是底数,第二个是指数,栈是先进后出,那么第二个参数就应该先入栈随后才是第一个,于是:
pushl $3 #指数为3 pushl $2 #底数为2
随后调用函数:
call power
这时候的栈形如:
这时,返回地址已入栈,在栈上,还有一个叫esp的指针始终指向栈顶,记住这点是很重要的,接下来我们要做的事是让ebp入栈,ebp是一个基址指针寄存器,让它入栈的原因是,它可以加上偏移量来获取栈内的所有需要用到的数据,为什么不用esp?esp也同样可以做到,但是esp有更重要的事,比如始终指向栈顶,那为什么不用其他的指针寄存器,确实,其他的指针寄存器也能达到同样的效果,但是ebp在x86架构中,会快得多。
push %ebp //ebp入栈
我们把esp赋值给ebp,这样ebp就能加上偏移量来获取数据了,值得注意的是,这里是赋值的指针,而不是具体的值
movl %esp, %ebp
这样是赋值的具体的值:
movl (%esp),%ebp
现在我们能用ebp加上偏移量来获取栈内的数据了,首先把两个参数保存至寄存器
movl 8(%ebp),%eax movl 12(%ebp),%ecx
这里的类型是long,所以每个栈帧相距4个地址
接下来的话,我们需要计算2的三次方,按照高级语言来说,我们可以嵌套一个循环
//假设a为底数,e为指数 for(;e == 1;e--){ a += a; }
每次循环e都会自减,直到e为1为止。循环体是a的自加,a的x次方就相当于a加上x -1个a
start_loop: cmpl $1,%ecx je loop_exit addl %eax,%eax decl %ecx jmp start_loop loop_exit: popl %ebp ret
cpml判断ecx是否为1,ecx里存的是指数,按照上的思路,每次循环,指数都会自减,如果为1,那就跳出循环,然后就是底数自加,过程很简单,我就不再赘述了
下面看到loop_exit,这是跳出循环需要执行的代码,换句话说,这时候函数已经要快执行完毕了,在函数要执行完毕时,eip需要拿到函数的返回地址,也就是下一条指令的地址,这个操作由ret完成,ret会将栈顶的栈帧弹出,然后获取里面的值,将eip指向这个值,因此,我们需要把函数的返回地址进行弹出。当前esp没有进行变动,它指向edp的栈帧的首地址,所以我们可以先弹出edp,弹出之后,函数的返回地址就位于栈顶了。
loop_exit: popl %ebp ret
然而有的时候esp不在edp栈帧的首地址,比如我们需要局部变量,我们需要把esp向低地址方向移动以获得额外的空间来存放局部变量,这时候我们还需要做的一件事情是将edp赋值给esp,让它移动到edp的栈帧首地址,再进行弹出edp,很好理解,对吧?
movl %edp, %esp popl %ebp ret
现在一个函数到此执行完毕,ret过后,esp还在第一个参数的栈帧的首地址,如果不再需要这些参数,我们可以在函数调用完毕时,把esp往高地址方向移动8个地址
addl $8,%esp
至此,一个函数的调用过程就结束了
总结
函数栈保存了一个完整的局部作用域,在函数调用完毕我们应该将局部变量和参数全部释放相关文章推荐
- linux平台学x86汇编(十九):C语言中调用汇编函数
- 浅析Linux从API调用到底层驱动的过程
- C进程的Memory Layout&linux进程的地址空间&函数调用过程
- linux驱动调试之段错误分析-根据栈信息分析函数调用过程
- linux-i2c驱动 之 i2c设备层的注册过程probe函数如何被调用分析
- linuxC一站式编程的函数调用的过程汇编理解
- 深入理解C语言----函数调用过程浅析
- Linux汇编---函数调用过程
- 【C++内存管理】浅析C++中函数调用时的内存分配-函数调用过程中其他函数相关的内存分布
- Linux汇编---函数调用过程
- 浅析linux 2.6.23 bus总线模型下match()和probe()函数调用顺序
- 分析linux下的进程地址空间,以及c语言的函数调用过程
- linux平台学x86汇编(十六):在汇编语言中调用C库函数
- 函数调用过程(转载)
- C语言(函数)调用过程(略译)
- linux驱动的多种init函数及其调用顺序
- MFC应用程序中处理消息的顺序,创建窗口的过程关闭窗口的顺序(非模态窗口),打开模式对话框的函数调用顺序
- Linux中与信号量有关的函数调用 semget, semop, semctl
- ffmpeg_sws_scale()__函数中的调用过程
- 【Linux】Linux添加系统调用以及内核编译过程