二、<代码讲解>应用眼中的操作系统 & 系统调用

file filename:打印指定文件的信息
ps ax|less:打印所有进程
ip address:查看ip地址
xxd + filename:可以查看二进制文件


hello.c如下

#include <stdio.h>
int main()
{
	printf("hello,world\n");
	return 0;
}

命令:
gcc -c hello.c
ld hello.o会报错无法找到入口,且没有定义puts
在这里插入图片描述
ld -e main hello.o,以main作为入口,链接hello.o
在这里插入图片描述
只剩下puts的报错了。


hello.c最简单的main程序

int main()
{
}

gcc hello.c
ld hello.o
在这里插入图片描述
就连最简单的hello都不能运行…

ld -e main hello.o
成功链接
./a.out
有一个段错误
在这里插入图片描述
用gdb 去看看运行过程:
starti:从第一条指令开始执行
layout asm:更方便的查看汇编
info register:查看寄存器

gdb a.out
starti
layout asm

输入 info register
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
若干次si后,停在retq
bt 打印出栈上的所有函数
retq上再si,就出错
在这里插入图片描述
显示非法的内存访问,因为栈上有其他数值,这个数值作为返回地址是不合法的。

从这个例子知道,操作系统帮我们做了很多事情,应用程序才能运行起来。

下面逐渐解开上面的这些困惑。
在这里插入图片描述
gcc -c minimal.S
ld -e foo minimal.o
gdb si 到第一个syscall停止,这是调用操作系统的API,这之前是设置调用的参数,表示write (fd=1,buf=0x400097,count=0x1c)
再si,打印出Hello,OS World(在这个过程中,操作系统会执行很多很多指令,可能上百万条指令,访问IO设备,最后显示到屏幕上)
si si 停留在第二个syscall,再si,这条指令是退出,然后就退出了。(exit(1))

man 2 syscall --可以查看syscall的man。

所以,在最简单的程序中,直接调用操作系统的API,让操作系统帮我们工作。


如果是在C程序,应用程序如果链接了C的标准库,他们的行为又是怎么样的呢?
上面的那个汇编对应的C代码为:

#include <unistd.h>
#include <sys/syscall.h>

#define LENGTH(arr) (sizeof(arr) / sizeof(arr[0]))

const char hello[] = "\033[01;31mHello, OS World\033[0,\n";
// try : const char *hello = ...;

int main()
{
	syscall(SYS_write,1,hello,LENGTH(hello));
	syscall(SYS_exit,1);
}

在这里插入图片描述
gcc -g syscall-demo.c:-g可以把源代码信息包装进二进制文件,with debug_info
如果加上-g,可以使用objdump -S a.out | less 查看a.out
/main找到main 的代码
在这里插入图片描述

可以看到有syscall@plt,表示这个函数是动态链接的,并没有在a.out这个二进制文件,而是在另外一个动态链接库,通过Procedure linkage table来调用。也就是说,main函数如果想要执行并且能够调用syscall@plt,在main执行之前,一定有一些代码帮我们把库函数加载到程序的地址空间。暂时还不知道这个过程是谁做的,但是我们可以确定在main函数执行之前,程序执行过很多代码了。


Question:一个普通的C程序执行的第一条指令在哪里?

main的第一条指令?错!
libc的_start?错!

gdb a.out
start i:停留在第一个指令
在这里插入图片描述

info inferiors:查看当前进程
(gdb) info inferiors
Num Description Executable
*1 process 35745 /home/kkbabe/ics/a.out
!ls:查看当前文件夹的所有文件(这个没用)
!pmap 35745:打印此时的进程地址空间的内容
在这里插入图片描述
可以看到,操作系统在执行第一条程序之前,已经加载好了我们的程序a.out,并且也加载好了/lib64/ld-linux-x86-64.so.2。这个ld...so就是操作系统给我们最初始的加载器,这个加载器可以帮助我们加载libc,再调用libc的初始化,再调用main

回头看,第一次失败的尝试,如果用ld链接会失败。第二次失败的尝试,用ld链接成功了,但是执行时segment fault。为什么会失败呢?这告诉我们,操作系统在运行普通的C程序,实际上是经过了一个很漫长的过程。如果想要写一个程序在操作系统上直接被加载执行,只得用第三次那种成功的尝试,即用汇编代码的方式。

很容易误认为程序的开始和结束就是整个进程的开始和结束,但有个小例子。

#include <stdio.h>

__attribute__((constructor)) void hello(){
	printf("Hello,World\n");
}

__attribute__((destructor)) void goodbye(){
	printf("Goodbye , Cruel OS World\n");
}

int main()
{

}

在这里插入图片描述
正常来说当main返回结束后不输出任何东西就返回,但是用了两个gcc build-in的attribute,一个叫constructor构造器,一个destructor析构器之后,依然打出了东西。说明main 的开始和结束不是整个程序的开始和结束。


main函数的执行不是件简单的事情,加载器、libc都做了很多工作。那么就有一个问题,在main函数之前发生了哪些操作系统API的调用?
刚才实验用pmap看过,在第一条指令执行之前,进程中并没有libc的存在,只有a.out和ld.so的二进制代码。若想知道发生哪些API的调用,需要借助strace(system call trace)工具。(trace即踪迹

strace ./a.out:
可以发现程序的地一个系统调用是execve,代表a.out程序第一次执行。
之后又会调用很多系统调用,这些都是加载器libc调用的。
libc完成初始化后,才会调用constructor,main返回后调用destructor

kkbabe@ubuntu:~/ics$ strace ./a.out
execve("./a.out", ["./a.out"], 0x7ffd2dd6d280 /* 63 vars */) = 0
brk(NULL) = 0x55c59a0e8000
… . . . . . … . . . … . . . … . . . . . . … . . . . . …省略
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), …}) = 0
brk(NULL) = 0x55c59a0e8000
brk(0x55c59a109000) = 0x55c59a109000
write(1, "Hello,World\n", 12Hello,World ) = 12
write(1, "Goodbye , Cruel OS World\n", 25Goodbye , Cruel OS World ) = 25
exit_group(0) = ?
+++ exited with 0 +++

可以看到write那里有点奇怪,本来应该是write(1,"Hello,World\n,12")=12...,但中间却夹杂了一个Hello,World,这是因为strace在系统调用发生之前就能trace到系统调用,会打印出系统调用的信息,而这个系统调用同时又往同一个终端打印helloworld。
如果不想看到这样的干扰,可以
strace ./a.out > /dev/null,/dev/null是特殊的设备文件,所有写入的数据都将直接被丢弃。

… . . . … . . . .
brk(0x559f20c5d000) = 0x559f20c5d000
write(1, "Hello,World\nGoodbye , Cruel OS W"..., 37) = 37
exit_group(0)
+++ exited with 0 +++

这里看到两个write合成了一个,这个在之后讲。 (估计是缓冲吧)


已经知道了简单的应用程序如何调用操作系统,其实所有的应用程序都是这种方式调用操作系统的。
在这里插入图片描述
比如打开一个gedit的图形界面程序,比如gcc,这些都和hello world原理一样的。我们的操作系统提供了非常小的API,其他所有应用程序比如python解释器,java虚拟机,都是这套系统调用上实现的。

以gcc为例,看看会发生什么系统调用:
gcc和刚才的不一样,他会启动额外的进程。
gcc执行,会经过预处理、编译、汇编、链接来生成可执行文件,这个过程是不是gcc真正发生了,gcc是调用cc1,as,rd,还是gcc自己实现了这个很大的功能?为了回答这个问题,我们使用strace工具,用-f参数(当创建子进程时去追踪)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值