关于我
欢迎来到我的博客!这里汇集了我对编程和技术的洞见和总结。本站内容分为几个主要类别,涵盖从具体技术实现到编程理念的广泛话题。
主要内容分类
- 项目工程:深入探讨技术的实现细节和理解。
- C/C++:围绕C/C++语言的技术点和编程技巧进行详细总结。
- 程序员哲学:分享程序员在职业生涯中应该具备的哲学理念和思考方式。
想要了解更多具体内容,您可以访问文章分类页面。
联系我
如果您有任何问题或想要交流,欢迎通过关于页面与我联系。
感谢您的阅读和支持,希望我的博客能为您的技术旅程带来帮助!
概括
该文档详细描述了在navy-apps中编译并执行客户程序的整个流程,特别是如何将程序编译到目标操作系统nanos-lite上。文档解释了如何通过ramdisk.img加载程序,以及操作系统如何初始化设备、设置异常处理、处理系统调用等。此外,还涉及了如何通过硬件抽象层进行设备初始化和异常处理回调函数的设置,以及如何通过加载器(loader)处理和执行ELF文件
执行程序角度
在navy-apps中,编译我们的客户程序,编译到目标操作系统上:nanos-lite
此时,nanos-lite中,resources.S定义了一个.data节,这里声明了一个全局符号,ramdisk_start,以及ramdisk_end.
这两个变量其实就是地址,后续用这个符号作为地址,把我们生成的ramdisk.img包含了.
在编译的时候,我们就可以引用ramdisk_start去访问我们的客户程序elf文件.
这里还有包含logo信息的处理方式,在一开始,我的printf实现,内部有一个buf实现有点小,在打印logo的时候,会报错.
现在调整大了,就可以debug.
在编译之后,我们的ramdisk_start就指向了用户程序的elf数据.
我们就根据其中的信息,将不同的偏移,大小数据块移动到指定的地址.(这里如何进行地址映射的,在pa2中分析过).
移动好数据之后,跳转到规定的入口地址,就可以执行了.
这是大致的逻辑
init_device
这里是对am中的ioe_init的包装,操作系统的接口,调用了真实的操作内存的接口.
进行基本的外设初始化.按照逻辑,一个操作系统的启动,确实应该要先把最基础的部件初始化好.从下往上做工作.
所以,这里做init_device.
init_ramdisk
初始化硬件,然后就是做属于nanos-lite操作系统的工作.将硬盘相关初始化好.
按照逻辑,机器启动,硬盘中的数据是一直存在的.是需要将硬盘数据加载到内存中,而不是要对硬盘做什么.
这里只是输出了关于我们做的img的相关信息.
init_irq
对异常处理进行初始化.这里的初始化工作和cte_init的工作一致.这里就是调用了cte_init函数.将do_event函数作为回调函数.
这里的do_event函数的定位就是操作系统处理函数.
之前都分析过关于cte_init函数以及参数的定位和作用.
init_proc
如果这里不做任何修改,程序会执行完init_proc,然后就会调用yield,这里的作用和之前一眼,传入-1到a7寄存器,然后调用ecall.
修改之后:
首先就是做一些未来会用到的东西,这里只关注异常处理的流程和工作.
naive_uload
调用naive_uload函数,传入了NULLNULL.
然后在naive_uload函数会调用loader函数.这里就是加载器,会把ramdisk_start指向的地方作为硬盘,从这里获取elf文件信息,然后做一些加载的工作.
当loader返回的时候,传回入口地址.然后naive_uload会将这个地址传唤为一个void(*)()类型的函数.
然后加上一对括号,调用这个函数.
这样就跳转到elf文件,去执行客户程序了.
loader加载器
为了验证可行性和正确性,也是先实现了一个最简单的针对测试程序的加载器,先是使用readelf -h和-l来分析elf文件的布局,然后根据返回的信息,对这些偏移地址的数据移动到目标地址.
这里调用的ramdisk_read函数和memset函数来帮助做这些工作,这些工作不难.
现在主要的功能就是要实现一个自动处理elf文件,或者多个elf文件的能力.
比如传入一个file,那么就编写一个循环去处理其中的信息,找到其中的load字段,然后做处理.
现在还没有实现这个功能,现在主要是做异常处理的正确性研究.
异常处理
这里需要对nemu中和am中提供的上下文扩展进行再次的流程叙述.
对于中断流程,之前分析过.其中关于在ecall中调用的raise函数,也分析过,也没有后续的逻辑改变.
这里主要是关于在汇编代码中,跳转到__am_irq_handle函数,执行这个函数进行重点分析:
在raise函数中,我们根据当前的status,来设置了mcause的值,不同的模式下,我们的mcause的值是不同的.
在__am_irq_handle中,我们不再是根据mcause的值来做事件分发.
mcause是根据发生异常的代码环境取值的,而不是根据我们的系统调用号来取值的.
所以我们不能通过mcause来做事件分发.
使用a7寄存器,这是riscv的标准系统调用号存储寄存器.
我们规定的系统调用,进行索引,然后对这个事情做事件分发.目前是分发了EVENT_YIELD和EVENT_SYSCALL以及EVENT_ERROR这三个事件.
注意,这里是am中,这里只做事件分发工作,对异常作出最早的定性处理.具体的,不同的性质的事件的处理,我们需要调用cte_inint中注册的操作系统处理函数.
操作系统处理函数
调用了我们的操作系统处理函数,然后从保存的上下文中,做后续处理.
调用event,索引不同性质的事件.
如果是SYSCALL时间,那么就操作系统内部的do_syscall函数,对不同的系统调用做不同的处理.
如果是YIELD事件,那么就将a0传入0,然后程序就退回到ecall指令的执行处了.
如果是SYSCALL.
那么进入do_syscall函数.
在这里,我们会对a7寄存器进行处理,索引出不同的系统调用.目前实现的yield,exit系统调用.
根据不同的要求,在这里对不同的系统调用做处理.
write系统调用
用户程序调用write函数,传入了fd, buf,以及count.
write会继续调用_write_r函数._write_r函数会继续调用_write函数.
参数的传递,主要的参数还是这三个.
最底层的实际处理就发生在_write函数中.
_write会将fd传入a0寄存器,buf指针传入a1寄存器,count传入a3寄存器,SYS_write调用号传入a7寄存器.
在__am_irq_handle中,进行初步事件分发.将其事件定为SYSCALL.
在do_syscall中,进一步处理.
处理a0寄存器的合法型,如果是1,就正常处理,如果是别的,先放下不处理,做一个assert断言终止这里行为,让程序直接退出.
使用循环,将a1寄存器指向的地址,使用putch输出a3次数.完事.这里还需要对buf的大小和传入的a3次数进行比较,选最小的.
避免出现buf溢出
神奇的map opration溢出
在hello程序中出现了内存映射访问的溢出bug.
为何会出现map的溢出,一开始我查看了hello的调用栈,没有发现任何问题,也查看了SYS_write的一系列调用栈,也没有发现问题.
怀疑是否是指令问题的深层bug在今天显现了.
打开了difftest,也没有报错.
没办法,只能使用gdb来深层debug了.
这里bug发生在pc=0x83004fb8.
通过使用gdb-multiarch工具,set architecture riscv:rv32之后反汇编了0x83004fb8的代码
并且加上了/m参数,这里显示是位于fiprintf函数内部.
可是我们的代码并没有调用fiprintf哈数
于是我从程序入口0x83004d30入口开始查看汇编代码的跳转情况.
0x83004d30是_start函数地址.
这里跳转到了call_main函数0x83004d38
在这里,执行流确实是跳转到了main函数0x830000b4
在main函数内部,跳转了第一个函数_write,0x83004a28
在_write函数内部,看到了执行__assert_func函数.
这里确实是我在编写代码的时候,故意添加的一个assert(0)代码.这是编程习惯,喜欢把程序截停在这里,以达到某些确定性想法.
按理说,应该执行我的assert(0)函数,然后退出程序,报错bad错误.
但是为何溢出呢?
这里的_write函数显示确实跳进了__assert_func函数0x83004da8,可以看到这里的地址与溢出bug发生的地址越来越近.
在__assert_func内部,看到了fiprintf函数的跳转指令.
这里应该就是bug的缘故.首先,这里的调用函数出乎了我们的意料,意料之外的事情发生,通常会发生错误.
而这里在执行fiprintf函数的时候,发生了溢出.
在fiprintf内部,是一个sw指令,通过sp来访问内存,出现了访问0x7ffffffc内存的错误.
这里由于是汇编代码,并不知道为何如此,
目前的解决方案,回到_write函数,将assert代码注释掉.
这里还是有这样一个疑问,为何在程序中调用的assert和在操作系统/am中调用的assert执行情况不一样.
前者是更深处的调用了__assert_func函数,这个函数并不是我们自己实现的,内部行为不是明确知道.
后者是通过assert宏,包装了printf函数和halt函数来实现,这些都是我们内部已知的,实现的行为.
所以会导致以上的问题出现.但是__assert_func内部的具体错误,现在不得而知.
printf的标准
hello程序中要输出一个hello world,调用了printf函数.
变参系统已经被聊烂了,之前分析过变参系统的作用过程.
printf函数内部是会调用_vfprintf_r函数,这个函数,声称是安全类型函数,比_vfprintf要线程安全.
stdio.h中声明了_vfprintf_r函数,定义在libc的src/stdio/vfrintf.c中
在vfprintf.c中,什么也没有,只有一个包含文件,包含了vfprintf.h文件.
所以具体的定义是在vfprintf.h文件中.
在这个文件中,确实是要调用write系统调用.并且将_vfprintf_r函数封装成了_VFPRINTF_R函数.
在这个函数内部进行很复杂的逻辑处理.
但是其中对于write系统调用和返回值的处理,需要好好分析一下.
在syswrite.c文件中,定义了write函数.
在printf角度,这个werite函数就是系统调用,但是在这里,还不够上真正的nanos-lite系统调用.
write对_write_r包装,返回_write_r函数的返回值.
在_write_r中,调用了_write函数,并且根据_write函数,阿里设置ptr中的_errno.
这里有个坑,就是在_write函数内部是调用了真实的系统调用.
调用了SYS_write系统调用,处理了传入的参数,然后将结果放在了a0寄存器.
但是在_write函数内部,需要注释掉_exit函数,然后return需要返回我们的结果,不能默认返回0.之前没有注意到这里,debug了好久.
就是printf内部会反复尝试调用write系统调用.因为返回值是错误的,需要重新尝试.
为何返回失败,会一直反复调用write
上面分析了,printf在这种情况下,会反复调用来尝试输出字符.
但是每一次的调用都是传入一个字母,count为1.表示每次只输出一个字母.
这个现象就是在printf内部,进行malloc申请缓冲区的时候,申请失败(这里没有实现sbrk),所以会逐个字母来反复调用write.
并且每次都是输出了H字母,但是返回是失败的,所以导致一直循环输出H.
堆区管理
低地址.text>.data->.bss->heap-> … <-statck
这里.data就是全局的初始化内存,.bss就是全局没有初始化的数据内存.
我们的堆栈的起始位置_end就是.bss段的结束地方.
每次当需要扩大堆区size的时候,就会调用sbrk()函数去扩大内存.
这里就是做的就是一个program_break记录,每次按照申请的数量去标识堆区的大小.
我们的_sbrk函数,调用了SYS_brk系统调用.
在系统调用内部,使用了_end变量,声明外部链接变量_end,然后读取_end地址来获得堆区的起始地址.
然后使用program_break来记录堆区的范围,并且通过a0寄存器返回旧范围.
在系统调用的时候,传入接受返回值的return_value变量地址,和increment变量.
就是说,SYS_brk系统调用,需要两个参数,而不是文档中说的一个参数.
两个参数,可以更好的处理数据传递并且能够明显介绍代码的涉及范围.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !