应用眼中的操作系统:系统调用

系统调用的过程

在这里插入图片描述

什么是应用程序?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所谓的应用程序其实就是一个普通的文件,然后它按照一个特定的格式,比如一般为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退出。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例如:
在这里插入图片描述
可以看到一些列系统调用。

从特殊到一般的推论,是不是所有的应用程序眼中的操作系统都是如此呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在地球迷路的怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值