关注
控制流,指的是一系列按顺序执行的指令。多控制流,是指存在两个或两个以上可以并发(宏观同时,微观不同时)执行的指令序列。比如你编写的多线程程序,每个线程就可以看成是一个控制流,多个线程允许多个控制流一起执行。
在我们学习编程的时候,如果不借助操作系统提供的线程框架,是无法完成多控制流的运行的。
本文我们先来剖析一下,我们的指令如何”莫名奇妙“的就切换到其它线程的。
1. 指令执行
不管你用的是什么语言编程,最后都要落实到 CPU 上,而 CPU 只认识它自己的语言,机器语言。机器语言对应的就是汇编指令。如下面的
x86 指令序列。
...
mov eax, 5
push eax
call 0x00401020
add 0x4
...
程序在执行时,本质上就是汇编指令在 CPU 上一条一条跑。对于单核 CPU 来说,永远只有一条控制流,也就是只有一条指令序列。所以,宏观上模拟的多线程程序,本质上还只是单控制流,所谓的多线程,只不过是一种被制造出来的假像!
2. 控制流切换
汇编指令在执行的时候,最重要地方在于它需要依赖 CPU 环境:
一套通用寄存器 (eax, edx, ecx, ebx, esp, ebp, esi, edi)
标志寄存器 eflags
指令寄存器 eip (eip 用来保存下一条要被指令的地址)
还有一个很重要环境,就是栈!因为指令序列在执行时,经常会保存一些临时数据,比如某条指令的地址。当指令执行
ret 指令的时候,cpu 会从当前栈顶弹出一个值到 eip 寄存器!这意味着要发生跳转了!
通用寄存器中,有一个寄存器名为 esp,它保存的是栈顶指针。指令 push 、 pop、call、ret 都需要依赖于 esp 工作。
call 指令把它后面的指令地址保存到 esp 指向的内存单元,同修改 eip
ret 指令把 esp 指向的内存单元中的值保存到 eip
push 指令把值保存到 esp 指向的内存单元
pop 把 esp 指向的内存单元的值取出。
图1 esp 与内存单元的关系
想象一下,如果某个时候,我们把 esp “偷偷”换了,换句话说我们是把栈换了,而栈中保存的那个“某条指令”的地址的值也不一样了,将会发生什么?
图2 更改 esp 寄存器的值做到栈切换
所谓的切换控制流,无非就是在更改 esp 栈顶指针来做到偷梁换柱的把戏而已。只不过,为了做到惟妙惟肖,我们在更改 esp 的时候,还得顺带的把通用寄存器环境修改修改,以适应新的那段“某条指令”的执行环境。
通常,这段新的“某条指令”的执行环境,恰好也保存在栈里,就像图 2 中 esp 到“某条指令地址”之间那段内存的数据。
说了这么多很抽象,我们来一个具体的例子。简单讲解一下,更改栈中保存“某条指令地址”的后果。
3. 修改栈改变程序执行流程
3.1 程序清单
代码
// test.c
#include <unistd.h>
#include <stdio.h>
void fun() {
while(1) {
printf("Hello, I'm fun!\n");
sleep(1);
}
}
int main() {
int a[5] = { 0 };
// 传说中的溢出攻击
a[5] = (int)fun;
a[6] = (int)fun;
a[7] = (int)fun;
return 0;
}
编译和运行
$ gcc test.c
$ ./a.out
运行结果
图3 修改栈中数据
3.2 结果分析
图 4 中,左侧部分是上面 C 语言代码的反汇编部分。中间的栈是原本的样子,也就是还没执行 a[5] = fun, a[6] = fun
以及 a[7] = fun 的样子。被蓝色虚线框起来的是进入 main 函数的返回地址以及 main 函数的参数。最右边的栈,是被修改后的栈。
图4 指令序列分析
可以看到后面的越界赋值导致蓝色框框中的数据被破坏,导致“某条指令地址”被更改为 fun 函数地址,也就是图 4 中汇编第 4 行指令
pushl %ebp 这条指令的地址。
当指令执行到第 31 行的 ret 时,栈是图 5 的样子:
图5 执行到 31 行 ret 指令时栈的样子
ret 指令等价于 pop eip,也就是把栈顶的值送入 eip 寄存器。于是,程序就跳转到了 fun 函数中执行,形成图 3 中的效果。
如果你阅读上面的执行流程感觉困难,建议你先读一读有关函数执行流程的知识,在文章《打造自己的 longjmp》中有很详细的介绍。
4. 再论控制流切换
在你彻底明白第 3 节的实验后,我们换个思路,我们不修改栈的内容,而是直接换一个栈,本质上也就是换 esp 的值,能不能达到相同的效果?比方说,新的栈里的内容,是我事先构造好的。再看一个实验。
代码
#include <unistd.h>
#include <stdio.h>
void fun() {
while(1) {
printf("Hello, I'm fun!\n");
sleep(1);
}
}
int main() {
int stack[64] = {0};
stack[63] = (int)fun;
// 新栈的栈顶指针
int new_esp = (int)(stack+63);
__asm__ (
"mov %0, %%esp\n\t"
"ret\n\t"
::"m"(new_esp));
/* 上面的这段内联汇编翻译成 x86 汇编是这样的:
mov esp, new_esp 切换栈顶指针
ret 返回
*/
return 0;
}
编译和运行
$ gcc test2.c
$ ./a.out
执行效果和图 3 中是一模一样的,这里就不贴图了。
这个实验和第 3 节中的区别就是不再修改栈内容,而是使用我们自己构造的新栈,以达到相同的控制流切换的效果。这里就不再画栈的样子了,留给读者自己分析。
这个真的是控制流“切换”吗,看起来像而已,本质上它只是个跳转。
5. 总结
理解“切换”的本质
掌握指令执行与栈的关系
练习1:完成本文中的两个实验。练习2:画出实验二中旧栈和新栈,分析是如何从旧栈切换到新栈的,以及是如何跳转到函数 fun 中去的。
查看原帖
点赞 评论
相关推荐
07-08 12:45
吉林大学 硬件开发 现在深挖技术还来得及:大厂
我这个地方基本部门组里就我一个 同事要不出差 要不就是另一个地方办公
实习搭子来的第二周就去上海出差了,到现在还没回来
点赞 评论 收藏
分享
牛客热帖
更多
正在热议
更多
# 实习生的蛐蛐区 #
46052次浏览 356人参与
# 夸夸我的求职搭子 #
199804次浏览 1917人参与
# 你认为小厂实习有用吗? #
16953次浏览 212人参与
# 三一重工求职进展汇总 #
13007次浏览 60人参与
# 应届生,你找到工作了吗 #
19352次浏览 144人参与
# 硬件应届生薪资是否普遍偏低? #
75053次浏览 518人参与
# 说说你知道的学历厂 #
32280次浏览 190人参与
# 计算机有哪些岗位值得去? #
14901次浏览 142人参与
# 下班后的时间你怎么安排 #
8860次浏览 129人参与
# 材料人,你们签了哪个公司 #
7181次浏览 17人参与
# 你找工作的时候用AI吗? #
16404次浏览 209人参与
# 面试尴尬现场 #
27781次浏览 187人参与
# 哪一瞬间觉得自己长大了 #
8134次浏览 183人参与
# 在职场上,你最讨厌什么样的同事 #
14911次浏览 151人参与
# 社会教会你的第一课 #
32148次浏览 420人参与
# 中核求职进展汇总 #
20498次浏览 152人参与
# 电网笔面经互助 #
36508次浏览 354人参与
# 简历当中有水分算不算造假? #
25873次浏览 380人参与
# 神州信息工作体验 #
16398次浏览 75人参与
# 学历贬值真的很严重吗? #
22256次浏览 162人参与