百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

CPU眼里的:setjmp 和 longjmp cpu_set_t

bigegpt 2024-10-16 07:49 31 浏览

setjmp、longjmp是函数吗?为什么使用起来跟其他函数有那么大的差异呢?这么奇怪的函数,它存在的意义是什么呢?


01

提出问题

setjmp 和 longjmp,可能是C/C++语法中,最让人沮丧、望而却步的API函数。为什么有人说它们是大神行走江湖的必备技能?为什么也有人说:掌握了它们,就掌握了一半操作系统的秘密?

网上从来不缺对它们的解释,但无论如何解释,它们跟普通函数,还是有很大的不同,几乎所有的函数概念,在它们身上都是失效的。

例如setjmp作为一个函数,虽然从代码上看,它只被调用了一次,但却可以返回两次,而且每次的返回值还可以不同。


02

认识 setjmp 和 longjmp

首先简单介绍一下setjmp和longjmp这对API。打开Compiler Explorer,让我们写一个简单的main函数;然后去调用函数func1,最后再用函数func1调用函数func2;为了便于展示这个调用过程,我们分别在函数的开头和返回阶段;打印了一些信息,展示函数的运行过程。代码和运行结果,如图所示。

如我们所料,函数是逐层的调用,直到函数func2;然后,再逐层的返回,直到main函数。

好了,现在轮到setjmp和longjmp登场了,我们先在main函数中,通过setjmp函数设置一个跳转点;并为setjmp传递一个参数context,用来保存上下文。这里我们先不解释,只是无脑的按照API手册操作就好。

根据API的解释,setjmp第一次返回的时候,它的返回值是0;也就是正常流程,此时我们就正常的调用函数func1就好;为了便于观察setjmp的返回值,我们把value作为整个进程的返回值。如图所示。

很遗憾,输出没有任何变化!由于此时仅仅设置了跳转点,但并没有作跳转。所以,从输出结果上看,函数仍然是逐层调用,再逐层返回的。返回值value的值也是0,这看上去,跟如图所示的普通的函数调用,没有任何区别。

随后,我们在函数func2中,做一次longjmp。并规定了这次setjmp的返回值是第二个参数:100,如图所示。

发现有趣的事情了吗?相比刚才如图上所示的函数调用,longjmp直接跨越了常规的函数返回流程。没有依次从函数func2返回到函数func1,再返回到main函数;而是,直接隔空跳转到main函数,甚至setjmp的返回值也变成了我们刚刚设置的100。

为什么会出现如此反常的现象呢?如果试图用函数的概念去解释setjmp和longjmp的行为。一定会掉入一个死胡同,因为它们根本就不是一个普通意义的函数,甚至,它们根本不能用C/C++语言实现!还好C/C++语言的教程很少提及它,否则可能又是一场激烈、且没有结果的讨论。


03

实现自己的 setjmp 和 longjmp

好了,与其让阿布去瞎编背后的实现原理,抛出一些是似而非的概念。不如我们亲自实现一个setjmp和longjmp来的大快人心。

实现的方法也很简单,我们直接借用CPU眼里的:上下文 | Context中保存、恢复任务线程(任务)上下文的办法,通过自己的my_setjmp函数把当前线程(任务)的上下文,全部保存在数组context里面。

这样在未来发生my_longjmp的时候,恢复一下上下文,就可以让函数func2直接跳回到my_setjmp需要返回的地方,从而实现my_setjmp的第二次返回。

说干就干,先定义一个my_setjmp函数,参数context是一个数组,用来保存上下文,也就是CPU寄存器,不过这不是C/C++这种高级语言可以做到的。所以,我们使用内嵌的汇编语言来实现这个想法。

long context[3];

__attribute__((naked,returns_twice))
int my_setjmp(void* context)
{
    asm("mov %%rbp, (%%rdi);"
        "mov %%rsp, 8(%%rdi);"
        "mov (%%rsp), %%rax;"
        "mov %%rax, 16(%%rdi);"
        "mov $0, %%rax;"
        "ret;"
        :::);
}

如上面的代码所示,首先把CPU中堆栈相关的寄存器rbp、rsp保存在数组context的第一和第二个数组元素里面;随后,为了让未来的my_longjmp,正确的跳转到my_setjmp的返回地址,我们需要把返回地址也保存一下。

“CPU眼里的:函数调用”所说,此时的函数返回地址就保存在堆栈的栈顶处(其内存地址,就是rsp的值)所以,我们直接把栈顶的内容,也就是返回地址,保存在数组context的第三个元素里面就好。注意,由于指令集的限制,我们不得不通过寄存器rax中转一下。

“CPU眼里的:参数传递”所说,寄存器rdi保存着context的内存首地址0x404020,分别经过0、8、16的偏移,正好对应着context的3个数组元素。如图所示。

简单起见,我们就不保存所有的CPU寄存器了,但完整的方案,是需要保存全部的CPU通用寄存器的。

最后,别忘了my_setjmp第一次的返回值是0。如“CPU眼里的返回值”所说,简单情况下CPU寄存器rax,用来保存函数的返回值。所以,我们把返回值0,写入到寄存器rax里面即可。一切就绪,现在就可以通过ret指令返回了。

由于,我们已经处理好了一切,为了避免编译器的画蛇添足,我们还需要添加一行编译选项:

__attribute__((naked,returns_twice))

至于my_longjmp,就是一个反向操作了,也就是恢复或重新设置当前线程(任务)的上下文:

__attribute__((naked,noreturn))
void my_longjmp(void* context, int value)
{
    asm("mov (%%rdi), %%rbp;"
        "mov 8(%%rdi), %%rsp;"
        "mov %%rsi, %%rax;"
        "jmp 16(%%rdi);"
        :::);
}

如上面的代码所示,通过类似的方法,我们将保存在context数组中的CPU寄存器值,如数的写回到相应的CPU寄存器里面,如图所示。

随后,把保存在寄存器rsi里面的返回值value,写入到寄存器rax用来做返回值。

最后,就是最关键的跳转了,由于my_setjmp的返回地址存放在context的第三个数组元素里面,所以,直接jmp过去就好了。

如你所见,所谓的第二次返回,并不是传统意义上的函数返回,而是一次跟函数堆栈无关的CPU跳转操作。

同样,为了防止编译器画蛇添足,我们也要加上一行编译选项:

__attribute__((naked,noreturn))

它们对应的C语言是这样的,有兴趣的同学,可以细品一下:

void my_setjmp(long* context)
{
    context[0] = rbp;
    context[1] = rsp;
    context[2] = return address;
    return 0;
}

void my_setjmp(long* context, int value)
{
    rbp = context[0];
    rsp = context[1];
    rax = value;
    goto context[2];
}

好了,看看运行结果,如图所示,运行结果与官方的setjmp、longjmp完全一致!


04

运行自己的 setjmp 和 longjmp

如果这些代码,已经让人有些迷失方向的话,也没有关系。让我们化身成最慢的CPU,实际的跑一下整个过程。

方便起见,我们去掉代码中与核心无关的打印代码,调整一下代码的布局,一切从main函数中,调用my_setjmp函数的地方开始,如图所示。

先把参数context的内存首地址0x404020,存入参数寄存器edi(rdi的低32位),如“CPU眼里的:函数调用”所说,call指令会先把返回地址0x401161压入堆栈。

随后,跳转到函数my_setjmp里面,如图所示。

前两条指令,根据寄存器rdi的引导,分别把CPU寄存器rbp,rsp的值保存在context[0], contex[1]里面;

随后的两条指令,把栈顶处的函数返回地址0x401161,放在contex[2]里面,由于指令集的关系,需要通过寄存器rax中转一下;最后把返回值0,存入在寄存器rax里面。

最后的ret指令,会取出当前栈顶的0x401161,并以此引导CPU跳转到main函数中继续运行(函数返回的细节,也可以参看“CPU眼里的函数调用”如图所示。

忽略数据在寄存器和堆栈中的置换细节,由于此时eax寄存器的值是0,所以,显然会执行函数func1的调用,call指令还是会先把返回地址0x401179压入堆栈。

随后,跳转到函数func1里面,如图所示。

函数func1的前两条指令还是常规的栈帧保护工作,具体细节可以参看“CPU眼里的:函数括号”,略过随后无意义的mov eax, 0指令,就可以进行函数func2的调用了,call指令还是会先把返回地址0x40114c压入堆栈。

随后,跳转到函数func2里面,如图所示。

前两条指令,还是常规的栈帧保护工作。这里,我们不用在意栈帧保护的细节,和未来恢复栈帧的繁琐细节。因为,my_longjmp的大戏,就要开始上演了!

第三、四条指令是传递参数,分别把context数组的首地址0x404020和返回值100,传递给寄存器esi(rsi的低4位)和edi(rdi的低4位)。

万事具备,就可以执行call指令了。call指令,会先把返回地址0x401143压入堆栈,随后,跳转到函数my_longjmp里面,如图所示。

前两条指令,如数恢复寄存器rbp,rsp的值,然后通过参数寄存器rsi,把寄存器rax的值设置成:100。

此时,除了用于返回值的寄存器rax值(100)不同于此前的0外,所有的寄存器信息,跟我们第一调用my_setjmp时,所保存的信息,完全相同!

最后的jmp指令,从context[2]中获取上次my_setjmp的返回地址,从而,指引CPU第二次回到main函数中继续运行,如图所示。

不过这次的返回值,也就是寄存器eax的值不再是0,而是100。这就是为什么my_setjmp会返回两次,而且每次返回值都不相同的原因。


05

总结

  1. 简单的说,setjmp和longjmp是一个增强版的goto。只是goto只能在函数里面,自由跳转;但setjmp和longjmp,则可以实现函数之间反常规的跳转。
  2. 跟goto一样,由于这种大范围的跳转,可能跳过一些必要的代码,例如:资源回收、释放内存等。所以,longjmp可以跳过常规函数之间的返回流程,从而带来巨大的效率提升,但也可能增加代码的维护难度和可读性。
  3. setjmp和longjmp已经非常接近操作系统的“上下文”保护。如果加以利用,完全可以在一个单线程中,实现任务切换,也就是所谓的:纤程、协程。

最后,阿布认为:似乎除了用CPU的视角,配合代码,实际跑一下setjmp和longjmp。否则使用任何概念或者类比的方式,解释这个问题,都是在把简单问题,复杂化。


06

热点问题

Q1:不用汇编语言,只用纯C语言,可以实现setjmp和longjmp吗?

A1:由于setjmp和longjmp的实现过程,需要精确的读、写CPU寄存器,所以首选汇编语言来实现setjmp和longjmp。

C语言编译器从底层接管了CPU寄存器的各种操作,这样就不需要程序员关注太多CPU的运行细节,可以专心关注程序的算法和运算逻辑,从而提升编程效率。

但也让程序员失去了对CPU指令和运行细节的精确控制,因此与CPU底层相关的功能,往往由汇编语言实现的,例如:setjmp、longjmp、任务切换、原子操作等。


07

更多知识

基础不牢,地动山摇!如果喜欢阿布这种解读方式,希望更加系统学习这些编程知识的话,也可以考虑看看由阿布亲自编写,并由多位微软大佬联袂推荐的新书《CPU眼里的C/C++》


相关推荐

无畏契约手游测试资格获取方法,安卓IOS下载教程

《无畏契约:源能行动》是拳头游戏与腾讯光子工作室联合开发的《无畏契约》正版手游,延续了端游的5v5战术射击核心玩法,并针对移动端进行了操作优化。游戏以快节奏的爆破模式为核心,融合角色技能系统、经济策略...

微软正在测试重新设计的Office图标 但您现在可以提前下载重制版本

今年4月,有消息称微软正在征求用户对一组Office图标7年来首次重制版的看法(上一次重制是在2018年末)。现在,有人决定自己动手,制作了一套微软的高分辨率图标包与用户共享以获得反馈。Reddi...

AB Download Manager:一款可以替代IDM的开源桌面下载管理器

软件介绍IDM下载器大家应该多少都知道一点,如果不知道的话只能自行百度了,但是IDM本身是需要付费的,而今天推荐的这款软件,在下载方面是和IDM差不多的,大概有90%的相似度,感兴趣的朋友可以体验一下...

《夺宝奇兵》PS5光盘仅20G:其余需联网下载

来源:游民星空【《夺宝奇兵》PS5光盘仅20G:其余需联网下载】据游戏测试账号“DoesItPlay1”在推特发布动态表示,《夺宝奇兵:古老之圈》PS5实体光盘只存储了20GB的游戏数据,其余内容需要...

薇姐聊诗词7:诗词创作韵部查询及检测工具

薇姐聊诗词7:诗词创作韵部查询及检测工具。·1、诗词创作中所用韵脚哪里找?平水韵:106部,分平声30部、上声29部、去声30部、入声17部,反映中古汉语语音体系。新韵:(中华新韵)14部,以普通话为...

阿里云国际站:怎样模拟高并发测试场景?

本文由【云老大】TG@yunlaoda360撰写一、使用JMeter安装JMeter:从JMeter官网下载并安装JMeter。创建测试计划:打开JMeter,创建一个新的测试计划。添加线程组...

Android Studio 新增 AI 驱动的测试和更智能的崩溃诊断功能

随着GoogleI/O2025大会的落幕,值得注意的是,谷歌在AndroidStudio中引入了几项新功能,旨在改善Android应用程序的开发流程。最新版本集成了更先进的AI工...

如何在本地测试PHP源码的网站

通常,我们测试自建网站或从网上获取的PHP源码时,若直接上传到服务器,出错后再修改会很麻烦,因此一般会选择先在本地电脑上进行测试。1、先下载喜欢的源码,很多网站提供下载,如源码论坛等。这些源码是现成...

显卡性能测试工具3DMark06的应用教程

显卡作为计算机的重要组成部分,也是主要的输出设备。在计算机系统中,图形处理性能的瓶颈往往在于显卡。若要评估显卡性能,用户可以借助专业的检测工具3DMark,判断显卡是否能满足当前需求,或者是否需要...

Downie4 安装教程(轻松获取视频素材)

效果一、准备工作下载软件链接:http://www.macfxb.cn二、开始安装1、双击运行软件,将其从左侧拖入右侧文件夹中,等待安装完毕2、应用程序显示软件图标,表示安装成功三、运行测试1、打开软...

如何使用瑞星杀毒软件的网速测试功能

下面为大家介绍瑞星杀毒软件的网速测试功能。1、打开安全工具,找到网速测试,点击下载后开启。2、打开网速测试页面,点击开始测试按钮。3、测试结束后,你就能知晓自己的网速了。(9744667)...

阿里云国际站:如何测试服务器真实带宽?

本文由【云老大】TG@yunlaoda360撰写基于命令行工具测试iperf/iperf3:服务器端:在服务器上安装iperf后,运行iperf-s或iperf3-s启动服务端,...

CentOS Docker 安装

Docker支持以下的64位CentOS版本:CentOS9(stream)更高版本...必须启用centos-extras仓库,该仓库默认启用,如果您禁用了它,需要重新启用。使用官...

Fast YOLO:用于实时嵌入式目标检测(附论文下载)

关注并星标从此不迷路计算机视觉研究院公众号ID|ComputerVisionGzq计算机视觉研究院专栏作者:Edison_G目标检测被认为是计算机视觉领域中最具挑战性的问题之一,因为它涉及场景中对象分...

aigc检测报告与查重监测报告

哈喽学妹学弟们!最近是不是都在忙着写论文呢?记得当初我写论文的时候,也被AIGC检测报告和查重监测报告搞得晕头转向。不过经过我的一番摸索,终于搞清楚了它们之间的区别和联系。来来来,学姐今天就来给你们传...