当前位置: 首页 > 科技观察

进程切换的本质是什么?你明白了吗?

时间:2023-03-19 21:27:01 科技观察

大家好,我是小风哥。我们都知道操作系统最重要的功能之一就是多任务能力,即可以运行超过CPU数量——也就是进程数量的程序。要实现这个功能,就需要在多个进程之间拥有有限的CPU资源。分配的能力,从程序员的角度来看,我们的程序一直在运行,但是从CPU的角度来看,程序实际上是“走走停停”的,程序的走走停停涉及到进程的切换,所以进程切换的本质是什么?本质上,函数调用和进程切换非常相似。可能有同学会有疑惑。这怎么可能?别着急,看完这篇文章你就明白了。函数调用我们先来看看函数调用。函数调用是这样的。函数A调用函数B,当函数B执行时,会跳回函数A(此时函数A和函数B在同一个进程):voidB(){...}voidA(){...}这个过程是这样的:函数B执行完之后,控制权就交给了A,所谓控制就是告诉CPU继续执行函数A。但是,你有没有想过,当函数A调用函数B,函数B执行完一定要跳回函数A?不一定,既然函数B可以将控制权转移给A,那么它就可以将控制权转移给函数C。听起来很神奇,对吧?函数A调用函数B,当函数B执行时,可以跳转到函数C,但是这要怎么做呢?我们来看一段神奇的代码。一段神奇的代码有这样的代码:#include#includevoidfuncC(){printf("jumptofuncC!!!\n");exit(-1);}voidfuncB(){long*p=NULL;p=(long*)&p;*(p+2)=(long)funcC;}voidfuncA(){funcB();}intmain(){funcA();return0;}想想这段代码运行后会输出什么?可能有同学会说,main函数调用funcA,funcA函数调用funcB。funcB函数看起来像一堆赋值。执行完成后返回funcA。funcA又返回了main函数,所以执行完不会有任何输出。真的是这样吗?编译运行吧(小枫使用的是gcc5.2.0版本,64位机器,没有开启编译优化,不同编译版本运行效果可能不同)。$./a.outjump到funcC!!!可能有同学会奇怪,这怎么可能!!!这段明明没有调用funcC,但是funcC函数为什么运行呢?程序员常说“代码中没有秘密”,这句话不完全正确,应该是“机器指令中没有秘密”,后来想了想,这句话也不完全正确,因为对我们来说,CPU如何执行机器指令其实对我们来说是一件非常重要的事情。对于黑匣子,我们只能从大体上讲CPU是如何执行一条机器指令的,而这里真正的细节只有Intel/AMD等处理器厂商知道,而一些魔鬼正是在这些细节中。细节决定成败,我们回到本文的主题,先看看生成的机器指令长什么样:0000000000400586:400586:55push%rbp400587:4889e5mov%rsp,%rbp40058a:bf74064000mov$0x400674,%edi40058f:e8bcfeffffcallq400450400594:bfffffffmov$0xffffffff,%edi400599:e8e2feffffcall400480000000000040059e:40059e:55push%rbp40059f:4889e5mov%rsp,%rbp4005a2:48c745f8000000movq$0x0,-0x8:4%rbp4005aa:48f85d4005aa:48f85d4lea-0x8(%rbp),%rax4005ae:488945f8mov%rax,-0x8(%rbp)4005b2:488b45f8mov-0x8(%rbp),%rax4005b6:4883c010添加$0x10,%rax4005ba:ba86054000mov$0x400586,%edx4005bf:488910mov%rdx,(%rax)4005c2:90nop4005c3:5dpop%rbp4005c4:c3retq这些指令在说什么?我们先看普通的函数调用:当函数B执行时此时的栈帧是:函数B的最后一条机器指令通常是:ret,这条指令的作用是将当前栈顶的内容弹出到%rip寄存器,CPU会根据rip指令中的值从内存中取出并执行。很明显,ret指令会将之前保存的返回地址放到rip寄存器中,这样CPU就可以继续执行A函数中后面的代码了,也就是++a这行代码有同学可能已经看到了,如果我们有办法修改A栈帧上的返回地址,是不是就可以实现“打哪里就打哪里”呢?事实上,行“*(p+2)=(long)funcC;”代码中将原本指向funcB的函数返回地址改为指向funcC,即当funcB函数运行完毕后,会直接跳转到funcC函数,从而实现可控的执行流程切换。进程切换的本质和这个是一样的,只不过进程切换需要伴随着栈也切换(还有地址空间),切换进程的上下文也要保存。可能有同学已经看到了,上面的过程叫做缓冲区溢出攻击。要达到的目的和进程切换一样:实现控制权的转移,但是缓冲区溢出攻击是非法的,不符合预期(符合黑客的预期,但不符合规则操作系统设计者设定的游戏),而进程切换是合法的和预期的(符合操作系统设计者的期望)。有时,(真正的)黑客和操作系统设计师实际上是同一个团队。