用户程序的编译、加载及执行流程详解

项目工程

Posted by Bruce Lee on 2024-09-15

关于我

欢迎来到我的博客!这里汇集了我对编程和技术的洞见和总结。本站内容分为几个主要类别,涵盖从具体技术实现到编程理念的广泛话题。

主要内容分类

  • 项目工程:深入探讨技术的实现细节和理解。
  • C/C++:围绕C/C++语言的技术点和编程技巧进行详细总结。
  • 程序员哲学:分享程序员在职业生涯中应该具备的哲学理念和思考方式。

想要了解更多具体内容,您可以访问文章分类页面。

联系我

如果您有任何问题或想要交流,欢迎通过关于页面与我联系。

感谢您的阅读和支持,希望我的博客能为您的技术旅程带来帮助!


概述

文档描述了用户程序的编译、加载、和执行过程。起始于libos库中的_start函数,通过call_main跳转到主函数,最终通过exit系统调用结束。用户程序被编译后放置于ramdisk.img中,相关信息记录在files.h中。加载过程中,naive_uload函数调用loader加载程序至指定内存位置。系统调用通过ecall后由AM层的异常处理到操作系统的do_event函数处理,支持文件系统的交互和更复杂的输入输出操作

用户程序

从libos库中的,start.S中,开始编译,_start是.text的首地址,位于我们链接的0x83000000处,生成的是位置相关代码.

这里的汇编程序,跳转到了call_main函数,然后在call_main函数内部调用了main函数.从此,开始了用户程序的代码执行.

从main函数返回之后,调用了exit系统调用,退出程序.

我们的用户程序经过make工程的编译,被放置在了ramdisk.img文件中,ramdisk中的信息被放置在files.h头文件中.

加载

init_proc函数,做了初始化.调用了naive_uload函数.

在这个函数内部,我们传入一个文件参数filename,比如是/bin/bmp-test.

然后naive_uload会调用loader函数.并且根据loader函数的返回值,跳转过去.这些在pa3.2文档中分析过.

现在,是加了文件系统的loader实现.

loader中,调用了一个加载单一文件函数do_load_file函数,这个函数只加载传入的filaname文件到elf文件中指定的加载位置.

所以,这个函数是通用性的,其要加载的位置,偏移,大小,都是根据传入的elf文件索引得到的ramdisk中的字节信息来决定的.所以后续的如果要多程序加载,需要更爱的就是elf文件中的头表信息.然后根据适当的算法去调用do_laod_file文件

do_load_file函数,调用fs_open函数,获取文件的描述符.

然后调用fs_lseek函数,定位到文件内偏移16字节位置,读取2字节出来,分析这个两个字节的值,如果是2,那么就是可执行文件,否则就直接报错,退出.

然后偏移到24字节位置,读取4字节出来,获取了程序跳转入口地址,用于do_load_file函数返回.

偏移28字节,读取4字节,获取了程序头表偏移地址.

偏移42字节,读取2字节,获取程序头表条目大小

接着读取2字节,获取程序头表条目数量.

然后开始for循环,查找每一个条目信息,读取首4自己,然后获取了头表信息,判断是否是load的类型.

如果是load类型的头表条目.

那么就把头表条目中的首32字节读取出来.

然后依次根据elf32的标准,获取了文件偏移,目标地址,文件占用空间大小,内存占用空间大小.

然后继续开始读取,buf改成我们的目标加载地址接可以了.

然后再调用memset函数置多余位置0.

系统调用

在am中的异常处理中,执行了ecall之后,调用的一系列函数,做第一步的事件分发.其中,根据a7寄存器的值,来判断这是一个yield函数一个syscall.

如果是syscall,那么就将其event设置为syscall,其他,同理.

此时我们的操作系统,在调用cte_init函数,注册的操作系统处理函数do_event,就是处理syscall和yield的.

可以说,cte_init中的注册函数,是我们实现的文件系统和系统调用与am底层的运行时环境的唯一授权点.

只有通过这里的注册函数,nanos中的功能才能够使用,否则就没有nanos.

在do_event函数中,进行事件识别.

这里的yield事件,做的处理,其实就是将返回值置为0,然后再次抛出了SYS_yield系统调用,使用halt终止程序.

如果是syscall,那么会有一个统一的管理函数do_syscall函数被调用.

该函数内部,根据a7进行事件识别,然后跳转到对应的事件处理函数.这里的策略和x86中一样.

引发中断,根据中断号,判断这是什么中断(这一步可以不要),然后根据中断号计算偏移地址,获取中断处理程序入口地址,然后跳转.

这里也是,根据a7寄存器的调用号,调用了不同的处理函数.

虚拟文件系统和系统调用

为何要把一切都抽象成为文件?

计算机的所有操作归根接地,就是计算和字节流的流动,这些数据都保存在内存中,无非就是根据逻辑计算,将一些字节流从一个内存到另一个内存罢了.

从最简单的现象进行抽象,那么我们就把每一块相关数据的内存视为一个块,这个块有自己的定义,展示自己的属性,数据类型,数据布局.其实这就是一个内存中的文件.或者说,这就是磁盘中的文件.

我们输入输出,是把字节流从内存移动到输入输出设备中,也是同样的原理,那么我们同样也可以抽象成文件.

规定,向stdout文件写入,就是向终端输出信息.从/proc/dispinfo文件读,就是获取了vga帧信息,向/dev/fb文件写,就是在向设备写入颜色数据,从而产生画面

所有的文件,都记录到了文件记录表中,这里记载了文件的位置,文件大小,磁盘的偏移地址,以及维护读写文件的读写头.

前面分析过这里的文件记录表.

哪怕是在navy中包装的printf函数,到低就是调用了write系统调用,write函数会直接传入1文件描述符,用于对stdout写.

__syscall_是navy中,唯一与操作系通的接触点.

前面分析的系统调用,如果是write系统调用,那么就会到sys_write的处理函数中.

不同的文件,有不同的读写方式,比如普通磁盘文件,就直接写读,如果是流文件,设备文件,那么写读就会有所不同.

所以在记录表中维护了每一个文件的写读方式.所以我们需要根据对不同文件的访问,来决定是否要调用其专有的读写函数.

有些文件,不允许读,或者不允许写,如果出现这种操作,直接报错退出就可以了.

对于普通的磁盘文件进行读写,需要掉用ramdisk_read/write函数,同时,也需要检查读写的位置是否存在越界情况.

如果是扩展文件,是否需要取消边界检查.对于磁盘的边界检查,是否需要做一些处理.曾经在这里踩过坑.

有一些,关于vga的fb进行抽象,是直接抽象成为了文件,然后对这个文件的写,就是对在nemu中的注册的映射内存空间写.

在这里需要做好相关的函数,对映射做好处理.

同时也要做好每一个系统调用的返回值的处理,如果没有处理好,那么libc中的一些安全性高的函数封装,就会产生一些难以理解的错误.

关于bmp

对于BMP_Load函数中的fopen和fread和flseek函数,我做了替换,换成了open,read,lseek函数.

相比较前者,后者行为我非常了解,因为出现了bug,我需要更好的定位,后来解决了,是关于磁盘访问边界处理的情况.

也没有改过来,能跑,就没改了.

NDL,就是对底层的包装,对系统调用的封装.没什么别的了.

曾经出现过对于ndl中的函数调用链接问题,后来解决了,通读了makefile代码,知道了整个make工程大致做的工作.

然后加了一个ndl的链接库,就行了.

其他的,至此,还没有实现更多的NDL函数或者miniSDL函数.


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 !