建立自己的函数调用帧
本文从最简单的打印“hello world!”的C程序开始,写出其汇编程序(在汇编中使用C库函数),讲解怎样建立自己的函数调用帧,接着使用jmp指令替代call完成函数的调转与返回。在linux内核中这种技巧被大量使用,最后举出内核中使用到的两个实例。
首先,下面的C程序完成的功能,相信大家学大多数语言,都是用来讲解的第一个示例:
1 2 3 4 5 6 7 8 9 | //helloworld1.c #include <stdio.h> int main() { printf ( "hello world!\n" ); return 0; } |
我们使用gcc进行编译生成可执行文件,结果如下所示:
1 2 3 | [guohl@guohl]$ gcc -o helloworld1 helloworld1.c [guohl@guohl]$ . /helloworld1 hello world! |
将上述C语言函数改成汇编程序,当然printf与exit函数还是使用C库自带的函数,这样就是汇编与C的混合编程,修改后程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #helloworld2.s .section .data output: .asciz "hello world!\n" .section .text .globl _start _start: pushl $output #通过栈传递参数 call printf #调用C库的 printf 函数 addl $4, %esp #恢复栈指针 pushl $0 #以下两行为 exit (0) call exit |
在这里开始调用printf与exit函数所使用到的栈帧是我们自己建立的,因为这两个函数的参数均是通过栈传递的,因此将参数入栈。从函数返回时,再恢复调用之前的栈帧。
使用as与ld分别进行汇编和链接,运行结果如下:
1 2 3 | [guohl@guohl]$ as -o helloworld2.o helloworld2.s [guohl@guohl]$ ld -dynamic-linker /lib/ld-linux .so.2 -o helloworld2 -lc helloworld2.o[guohl@guohl]$ . /helloworld2 hello world! |
在此程序中call指令实际完成了两件事——将下一条指令的地址压栈和将当前程序指针指向调用函数的入口。这样接下来就开始执行调用函数的程序,当从函数中返回时,ret指令恢复将之前压栈的下一条指令恢复到程序指针寄存器。
下面我们将call指令完成的工作重写一遍,得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #helloworld3.s .section .data output: .asciz "hello world!\n" .section .text .globl _start _start: pushl $output pushl $1f #将标签为1处的地址压栈 jmp printf #jmp到 printf 函数入口处,不是call 1: addl $4, %esp pushl $0 call exit |
在这里由于使用的是jmp指令而不是call指令,因此如果没有第11行的压栈指令,当程序从printf函数返回时,ret会将栈顶的值弹出到程序指针寄存器(即ip)中,对于本实验就跳转到数据段output那里了,这样就会出现段错误。因此,我们需要人为将函数返回时应该跳转的地址压栈,对于本程序即标号为1的地址。
与前一个实验一样编译链接并执行,得到结果如下:
[guohl@guohl]$ as -o helloworld3.o helloworld3.s [guohl@guohl]$ ld -dynamic-linker /lib/ld-linux.so.2 -o helloworld3 -lc helloworld3.o [guohl@guohl]$ ./helloworld3 hello world!
也许你就疑惑了,明明我用一个call就可以搞定的事,你为什么要用push和jmp两条指令完成呢?试想一下,如果我们不希望函数返回时执行到call的下一条指令,而是执行我们指定的一段程序,那么怎么实现呢?这时,将那段程序的地址先压栈,再通过jmp而不是call到调用函数,这样从函数返回的时候,就能执行到我们指定的程序段了。
下面举出内核中一个例子,使用的就是这种技巧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | #define switch_to(prev, next, last ) \ do { \ /* \ * Context-switching clobbers all registers, so we clobber \ * them explicitly, via unused output variables. \ * (EAX and EBP is not listed because EBP is saved/restored \ * explicitly for wchan access and EAX is the return value of \ * __switch_to()) \ */ \ unsigned long ebx, ecx, edx, esi, edi; \ \ asm volatile( "pushfl\n\t" /* save flags */ \ "pushl %%ebp\n\t" /* save EBP */ \ "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ "pushl %[next_ip]\n\t" /* restore EIP */ \ __switch_canary \ "jmp __switch_to\n" /* regparm call */ \ "1:\t" \ "popl %%ebp\n\t" /* restore EBP */ \ "popfl\n" /* restore flags */ \ \ /* output parameters */ \ : [prev_sp] "=m" (prev->thread.sp), \ [prev_ip] "=m" (prev->thread.ip), \ "=a" ( last ), \ \ /* clobbered output registers: */ \ "=b" (ebx), "=c" (ecx), "=d" (edx), \ "=S" (esi), "=D" (edi) \ \ __switch_canary_oparam \ \ /* input parameters: */ \ : [next_sp] "m" (next->thread.sp), \ [next_ip] "m" (next->thread.ip), \ \ /* regparm parameters for __switch_to(): */ \ [prev] "a" (prev), \ [next] "d" (next) \ \ __switch_canary_iparam \ \ : /* reloaded segment registers */ \ "memory" ); \ } while ( 0 ) |
这时进程切换的核心代码,切换的具体过程就不赘述,可以参考我的一个PPT,下载地址http://wenku.baidu.com/view/f9a17542b307e87101f6968d.html?st=1。重点看第17行和19行,第17行将希望即将切换进来的进程next的执行的ip压栈(而此ip是在next被切换出去之前执行siwtch_to在第16行所保存的,即标号为1的地址),在第19行调转到__switch_to函数,待到从__switch_to函数返回时,此时就可以恢复执行next进程从上一次切换出去的地方(即标号为1)继续执行。如果按照此情景,完全可以将17和19行换成一句“call __switch_to”语句;关键地方在于,如果是fork新建的一个进程,第一次调度,它之前并未执行switch_to的语句,因此切换到它的时候,并不能让它从标号为1的地方开始执行,而是应该让进程从sys_fork系统调用中返回,该地址在sys_fork->do_fork->copy_process->copy_thread 函数中进行赋值:
1 | p-> thread .ip = (unsigned long ) ret_from_fork; |
这样对于切换到新进程第17行压栈的将不是标号为1的地址,而是ret_from_fork的地址。可以看出,在这里,设计的非常巧妙!而且还是必须的!
邮箱:haifenglinying#yahoo.cn (#->@)
个人主页:www.hazirguo.com

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述