系统调用的过程
什么是应用程序?
所谓的应用程序其实就是一个普通的文件,然后它按照一个特定的格式,比如一般为ELF格式存储在磁盘上,然后我们就可以加载它运行它。
应用程序(Hello World)怎么调用OS?
现在我们想要做的事情是实现一个最小的应用程序它和这个OS交互打印一个Hello World,然后这个程序就退出。
下面是一个失败的尝试:
我们可以试一下上面这个事情:
在XShell上创建一个hello.c文件:命令为vi hello.c
保存退出,先按ESC,然后:wq敲击回车。
现在我们预处理已经完成,使用gcc对该c文件进行编译(gcc是Linux自带的):
编译完成后可以看见文件目录中多了一个hello.o文件:
hello.o是一个二进制目标文件,用file命令可以查看该文件类型:
可以看见hello.o文件为一个可重定位的(relocatable)ELF二进制文件。
现在我们可以使用ld命令去链接这个二进制文件:
可以看见结果和我们开始的PPT上展示的一模一样,一开始是给了一个Warning,然后下面给了一个错误:undefined reference to ‘puts’。
这个错误新手可能会经常遇到,就是所用到的函数未定义错误。
为什么我们命名写的printf而它显示的是puts函数未定义呢?
其实是因为gcc在编译时会进行一定程度的编译优化,虽然我们写的printf但是最后gcc执行时依然采用的是puts函数,即gcc会将printf替换成puts函数进行编译。
而在我们的hello.c文件中是没有定义puts函数的,所以会出现未定义错误。puts函数是C语言标准库(libc)中的一部分,我们其实只要把C标准库libc给一起链接进来就可以运行成功。
但是如果我们使用了C标准库的话,就违背了我们的初衷——我们要做的是一个最小的HelloWorld程序来启动以后就打印HelloWorld然后打印完了就走,而把libc拉进来的话就不是“最小”了,所以不能这样做。
还有一个问题是,我们都知道C程序是从main函数作为入口执行的,可是该程序链接时依然报警告:cannot find entry symbol_start(找不到入口标志_start)
这就意味着main函数实际上并不是一个C程序在二进制意义上真正的入口,链接器默认的入口其实是_start。
所以我们可以有两种方式来避免掉这条warnning,一个是将main函数改为_start;另一个是用-e命令指定。
可以看见warning没了。
我们刚刚遇到的问题是缺少标准库puts,那么我们可以再进行尝试,将除了main函数外的所有内容注释掉。
然后我们再去用gcc来编译:
链接它:
可以发现这时候我们成功了,没有任何的warning和error。
得到了一个a.out的可执行文件(executable):
下面是对a.out文件的说明
而当我们执行该文件的时候,它依然会出现问题:
可以看见并没有像我们预期的那样,从main函数返回0(如果main函数不给返回值那么默认为0),而是给了一句segmentation fault(段错误)。
连运行一个这么简单的程序如果不接入库函数(库函数在我们运行程序时已经完成了很多的事情)的话都不行究竟是为什么呢?
现在我们需要一个工具来帮助我们观察程序的执行。
来试一下这个程序,用gdb a.out:
(注意没有gdb的话得下载安装,用命令sudo yum -y install gdb)
现在我们得到了gdb的命令行,那么我们可以使用start帮助我们从第一条指令开始执行程序(我的版本用的是start命令,各个版本好像不一样…):
可以看见停在了main函数入口,现在我们用layout asm去查看一下汇编:
这样我们就可以看到汇编代码,这里我们也可以输入命令info registers查看各寄存器的值:
为了知道为什么这个程序会segmentation fault(段错误)我们可以进行单步调试(用si指令):
像上面那些指令都是可以正常进行的(如push、mov、pop啥的),但是有趣的地方在于return(即上面的retq)。return这条指令的行为是从栈上弹出返回的地址,然后和return配对的是call指令,当我们有一条call指令的时候,我们会把这个返回的地址放到堆栈上,然后跳转到main执行,而这个main函数到底是谁调用的呢?其实没有人调用main,是OS帮我们加载了这个main函数。系统调用栈上只有一个函数main,所以如果执行return这条指令的话,就会产生一个非法的内存访问,因为栈上还有一些其他的数值,而这些数值作为返回地址来说是不合法的,所以这种情况下程序就崩溃了。
我们可以通过readelf -h a.out(readelf -h显示ELF文件头信息):
我们可以看到上面有个Entry point address(入口点地址)为0x4000b0,这正好是我们的main函数地址。
这个就要写汇编代码了…(这课程太硬核了)
汇编不好懂的话还有一个C语言的版本:
用syscall这个库函数去执行系统调用,但是需要链接libc。
把上面说的总结一下:
应用程序(比如我们刚刚说的Hello World程序)作为最小的程序是如何调用操作系统的,其实就是通过设置正确的参数和使用syscall指令去调用操纵系统的API,就可以让OS帮我们完成很多的工作。
那么作为C语言,它们运行使用了标准库的行为是如何操作系统调用的呢?这个也需要分析一下(以刚刚提到的C代码为例):
main函数中就是使用syscall这个指令去调用SYS_write这个系统调用,往编号为1的文件描述符里写入地址为hello缓冲区上的LENGTH(hello)这么多字节,然后调用SYS_exit退出。
例如:
可以看到一些列系统调用。
从特殊到一般的推论,是不是所有的应用程序眼中的操作系统都是如此呢?