Skip to main content

NJU PA4

PA4.1

 实现上下文切换

之前CTF有种攻击方式叫做SROP,就是通过在栈上伪造假的context,再通过sigreturn这个系统调用恢复这个伪造的上下文,从而控制pc指针和一些参数指针,执行类似system(“/bin/sh”);的操作getshell(为什么操作系统要给用户提供这样的一个接口呢,莫名其妙)

kcontext函数我虽然还是没搞懂他的意义(感觉得去学学进程估计我才会懂),但是原理我很清楚了。

栈是从上到下的,也就是从stack.end->stack.start

这个union将cp指针放在了stack.start的前4个字节,用来标识这个stack的context在stack的位置,以便于操作系统总能通过cp指针访问到context(因为cp指针是固定的),每次进程切换时,context的指针被os获取(mv a0, sp),os将这个指针保存在cp上,再拿出另一个进程的PCB的cp指针将他返回,之后通过(mv sp, a0)先将栈切换,再将切换的栈上的值推入CPU中,实现上下文切换,进程切换

(这里很有意思哎,如果stack满溢出来,将cp精心构造为一个预设好的值,就能将进程切换到任意位置了,还能控制任意上下文,并且还能控制栈的值)

至于kcontext的传递参数嘛,我就将参数放进context里面的a0寄存器了,看起来能跑。

RT-Thread

这是ysyx关于pa的最后一个任务,后面ysyx就不要求了,可是我nanos-lite做都做了,肯定是得做完的啦,我之后考虑下是先做pa还是先做CPU,等我做完pa4.1看看我的意愿好了(估计会先做cpu,因为我觉得真实的硬件可以让我更加理解pa的内容,那这样的话pa4估计又要鸽挺久的,pa这么好的一个项目,不舍得结束啊)。

做之前没有看到后面的提示,写出了一个极其愚蠢的错误,我在rt_hw_stack_init()函数定义了一个局部变量,把局部变量的指针传给了kcontext,恢复上下文的时候把这个指针推给我的包裹函数的时候,这个指针指向的内存已经被释放了,解引用导致UB,我觉得应该要把这三个参数的值丢到各个的栈上面,但是具体应该放在栈的那个部分呢,最前面和最后面都不行,那我就直接丢在中间的某个随便的位置可不可以呢,让我试试。

试了一试似乎是可以的,只是代码写的有点丑陋,直接丢在-2的位置了….

感觉后面的这个“危险的全局变量(4)”怎么有点看不太懂捏,讨厌rtt,只想做nanos-lite

想了一想忽然悟了一些,yield()异常实现对于上下文的切换,yield()函数在rt_hw_context_switch调用之后是不会立马返回的,rt_hw_context_switch函数调用本质上来说就是一个栈空间的开辟以及在这块栈上进行一系列的操作(一些计算和syscall以满足函数功能),也就是说这个函数实际上在调用完yield还没有返回,也就是sp还没有+,栈空间还未释放,之前定义的那些局部变量还存在于栈上也就是还是可以被访问的,可以想像yield就是一个普通的函数调用,只是他返回的时机是这个线程(或者进程)被再次唤醒的时刻,上下文其实就是一个进程的所有状态,他只是在被切换时被时停了而已。

于是,若是在调用这个函数的时候定义一个局部变量来存储user_data部分的值,然后在yield返回之后恢复这些值,就不会破坏上面原本的值,而在yield这个异常处理的过程就能随意改变这些值。

(不出所料报错了,啥时候能一次把代码写对呢,哎)

bug可能出现在很多地方,比如这次的bug就出现在我还是理解错了…….

更正一下理解,user_data里面的值初始值是0,因为估计根本没有把他当作指针,这样来说,我们就可以将他当作指针存储我栈上的from和to的地址,但是我发现最后的恢复似乎没有被执行…..,因为似乎现在并没有切换回来的操作,这样我怎么知道我实现得对不对捏,难受。

(贴一下代码,从白变黑的原因是现在是晚上了……)

感觉写的不好555

之后的内容需要native启动,但是我native不知道为啥一直运行不了(之前就不行,可能是macOS用户特有的苦难吧)

Nanos-lite

这个跟yield-os一样,略

用户进程

创建用户进程的上下文

本来一直看不太懂,睡了一会起来就看懂了

之前一直都是通过naive_uload加载用户程序,这样肯定会有问题,原因就是没有创建用户进程的上下文,用户进程是通过一个函数调用实现的。

于是这个任务就是扩展naive_uload函数的功能,创建上下文,并通过上下文切换来执行程序,既然需要创建一个进程,那就需要对应创建一个PCB,并在PCB上布置相应的初始上下文,使得切换到这个程序时能以一个合适的上下文运行,这个工作交给了am的ucontext函数(于kcontext函数差不多,只是暂时不需要传递参数,并且kcontext是用来创建内核线程的,一定要分清内核线程和用户进程的区别),内核线程使用的栈就只是pcb中的栈(这是因为sp指针被恢复时获取到的是a0的值,而a0是返回值,返回context),内核线程之后再通过sp的移动将栈的上下文推到cpu里面(包括参数a0的值,这里可能容易搞混),用户进程在trap.S函数结束之后,sp的值依然还是指向的pcb中的栈,于是需要在用户进程的开始切换sp为用户栈来实现上下文切换的最后一步(切换sp),而之前会将用户栈的地址保存在a0中,通过mv指令实现切换。

所以说,用户进程的创建和内核线程的创建的区别就在于最后一步:sp的切换(但是我试了一下,就算没有这一步程序照样跑的挺好看的,原因在于这是自己编写的用户程序,可信任度和内核线程的程序差不多,基本不会导致内核栈溢出)

用户进程的参数

我希望有朝一日我可以深入阅读ABI手册,并思考“为什么这样约定”的原因,立一个flag好了,之后做完pa4.1之后做NPC的同时,抽出时间做这件事💪

参数argv如何推断出他的元素个数,不能用sizeof(应该,我也没试过),因为argv本质上是一个指针的传递,元素个数在这个作用域未知,所以argv和envp约定以NULL指针结尾,当然,在用户程序中直接看argc就行了

这就是一个简单的指针练习,如讲义所说,至于后面的说要我自己RTFSC来跳过仙剑还是暂时算了吧hhh,看仙剑的代码很难受的一点就是需要特别熟悉那些库的api的行为,偷个懒,我直接在callmain中打印来看我实现的对不对好了

之后就是通过execve来执行新的进程了,execve的行为是结束当前进程,开启下一个进程并实现传参,那么实际上pcb就是顺延给了这下一个进程,包括cp指针和内核栈空间,但是先前的用户栈是不能被顺延给现在的进程的,原因是nanos-lite还在使用先前的用户栈,如果现在就对用户栈进行修改很可能会对执行造成影响,于是需要使用new_page函数分配一个新的空间而不是直接使用heap.end

之后看了一下这个new_page函数,也是从heap.start附近开始分配内存,我就想这不会和klib中的malloc函数冲突吗(malloc也是从这个附近分配内存),后面自己琢磨了一下,nanos-lite这个am程序根本就不会调用malloc,无论什么时候都不会调用malloc就行,然后我就想用户程序调用libc的malloc是分配的那个地方的内存呢,我就去看,发现是程序_end之后的内存(不是am程序的_end,是用户程序的_end,也就是超过0x83000000)的地方,之后我又想,am程序被加载到0x80000000处,但是am程序的栈区大概是在0x82xxxxxxx-0x87ffffff处,那这样岂不是有可能会覆盖掉用户程序0x83000000的代码和数据,于是就有了讲义的这段话

所以我最后觉得有必要先理一理内存的关系再来做这道题目:

1.首先是操作系统的程序,开始于0x8000000,是cpu上电执行的第一个程序(当然,现实中肯定不会直接执行操作系统),他的内存布局由链接脚本指定

当链接器将操作系统整合为一个ELF文件之后,由objcopy来将这个ELF文件进行类似操作系统中的loader相应的操作,预加载,将其生成为一个bin文件,在nemu中直接将这个文件copy到0x80000000的位置上(现实中应该由人手动将bin文件烧录到板卡的某个位置吧,这个我不懂555),于是这个操作系统程序就有自己的stack和heap了,heap的大小是从_heap_start开始到所有内存的结束位置(0x82xxxxxx-0x87ffffff)

2.之后是用户程序

在用户程序被加载之前,程序被存放在ramdisk中(现实中一般在disk上面,由驱动程序获取(我猜的)),ramdisk存放在操作系统的data section(就是上面的链接脚本)

之后由在pa3写的loader加载进入0x83000000的位置,在这个位置附近,是程序的代码段,数据段,bss段,堆区,会发现没有栈区,栈区分为内核栈和用户栈,内核栈在pcb中,而pcb在操作系统的bss段上面(因为pcb数组是os定义的一个全局变量),用户栈(暂时)在heap.end附近,也就是0x87ffffff处,但是现在的任务就是改变用户栈的位置到heap.start附近,这个heap是操作系统的heap,也就是由操作系统的heap来充当用户程序的stack(虽然感觉很奇怪,但现在我就只知道这么多了),还有一个奇怪的地方,就是操作系统的栈区在heap和bss中间,可能是因为之后的运行基本不会使用os的栈的缘故吧。

这样缕清楚就很舒服了

实现new_page分配内存给用户栈,PGSIZE是4KB(对齐要求吧可能是),之后在uload中调用new_page来分配用户栈,最后再实现execve即可

bug:nterm的运行pal和bird总是报错,但是运行其他的程序是可以的,但是用menu运行pal是不会报错的,并且在nterm运行menu再运行nterm,这个时候运行pal不会报错,如果不使用execvp直接用execve+/bin/pal,nterm运行不会报错(你说这种bug怎么解决,给我的信息太诡异了,并且错误信息也很少,吐了)

放弃任务:编译busybox会报很多错,无语了………,并且我都不知道怎么解决,这些错误很诡异:需要的头文件newlib并没有……..

ok,之后我就会退出PA一阵子了,可能会退出很久,特别是这最后的两个东西让我有点难受啊阿啊阿啊阿啊,之后我会先去做CPU方向了。