第二课 函数调用

时间:2022-02-17 00:50:05

赵连讯 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

本课程是后来补充进来的,因为错过了提交时间。

三大法宝

计算机是如何工作的呢?这里有三大法宝。他们是什么呢?第一个是存储程序计算机,第二个是函数调用堆栈,第三个是中断机制。
存储程序计算机的决定了程序运行了基本方式。

堆栈

堆栈机制是高级语言得以运行的基础。我们这里主要讲的是栈。堆栈是C语言运行时,必须的一个记录调用路径和参数的空间。

对堆栈的使用,是由编译器完成的。所以不同的编译器可能实现堆栈时,实现的方式是不同的。所以我们在编译相同的代码时,使用了不同的编译器,可能编译出不同的汇编代码。这是不足为奇的,只要他的功能是一致的。

堆栈的基本操作

堆栈上涉及的寄存器有:
esp
ebp
esp存放了栈顶指针;ebp存放了栈的基地址指针。
在后续的课程中总是不理解ebp的实际作用。总是感觉ebp并不是那么必不可少。后来又重新思考了一遍。ebp和esp组成了当前函数的栈的开始和结束,代表了一个独立的栈空间。超出这个范围的就是别人的栈空间了。因此从这个角度上来讲ebp是比不可少的。并且在子函数中可能会访问父函数栈中的空间。就是通过ebp来访问的。esp只在自己的栈中访问。两者各司其职。

常见的堆栈的操作命令是:
push 压栈
pop 出栈
call 完成函数调用,将当前的eip压入栈中,并将要跳转的位置更新到eip中
ret将返回的地址弹出栈到eip中。

过程

call func:
pushl %eip
movl next %eip//下一条指令的入口指令已经被压栈,压入了父函数栈中
建立函数调用框架
pushl %ebp
movl %esp,%ebp//达到了清空栈的功能,开始了子函数的栈

//函数本体的汇编代码
拆除函数框架
movl %ebp,%esp//esp指向的位置存储了父函数栈的ebp的值
pop %ebp//回到了父函数的栈
ret:pop %eip//执行下一条指令,并且此时esp指向了父函数的栈的栈顶

入参

我们在上面的分析中,没有看到传参的过程。
其实在调用call函数之前,父函数已经将要传输的参数压入到自己的栈中,并且紧紧挨着子函数的ebp。当子函数使用参数的时候,就用自己的ebp来访问入参。
这里,了解到入参不在子函数的栈中,而是在父函数的栈中。这与自己原来的理解不同。。。

返回值

函数返回值使用寄存器eax传递

函数调用汇编示例

C源代码:

#include <stdio.h>

int main()
{
printf("hello world!\n");
}

汇编代码:

    .file   "hello.c"
.section .rodata
.LC0:
.string "hello world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits

删除上述中的.开头的行数为:

    .file   "hello.c"
.section .rodata
.LC0:
.string "hello world!"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
ret

上述汇编代码非常简单,不在讲解。希望看客们自行分析。。。。