可执行程序的装载
一、预处理、编译、链接和目标文件的格式
以我们常写的helloworld为例。我们编写了一个helloworld的.c文件,我们来把它进行预处理,预处理就是gcc –e –o hello.cpp(预处理的中间命令)hello.c –m32,这里就把.c的文件预处理了一下。我们可以看到hello.cpp里面有前面的include.h文件,如果里面有宏定义的话,把宏定义都替换一下,实际就把字符串做一个处理。hello.cpp我们把它编译成汇编代码hello.s。然后我们再将汇编代码编译成目标代码hello.o。这时候我们就都得到了一个二进制文件hello.o。之前的文本文件都是可读的,这里的hello.o是乱码。他是二进制文件,里面有一些机器指令,不是可执行文件。把它链接成可执行文件,这个命令大家就比较熟悉了,gcc –o hello hello.o –m32。hello.o与hello都是elf格式的文件。
处理过程的命令如下
gcc –e –o hello.cpp hello.c –m32
gcc -x cpp-output -S -o hello.s hello.cpp -m32
gcc -x assembler -c hello.s -o hello.o -m32
gcc -o hello hello.o -m32
gcc -o hello.static hello.o -m32 -static
接下来看一下目标文件的格式,有哪些是目标文件?常见的文件格式,一个是A.out最古老的目标文件格式,然后后来发展成coff,现在我们常用的pe、elf(是一个文件格式的标准)。但是目标文件我们一般也叫它ABI(应用程序二进制接口),实际上在目标文件里面它已经是二进制兼容的格式了,什么叫二进制兼容的,也就是说它这个目标文件已经是适应到某一种cpu体系结构上的二进制指令。比如说我们在一个32位 x86编译出来的目标文件链接成arm上的可执行文件肯定是不可以的。这地方就一个二进制兼容的叫ABI。如图
- elf文件格式中的三种主要目标文件:
- 一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。(主要是.o文件)
- 一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
一个共享object文件保存着代码和合适的数据,用来被下面的两个链接器链接。第一个是连接编辑器[请参看ld(SD_CMD)],可以和其他的可重定位和共享object文件来创建其他的object。第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。(主要是.so文件)
当elf文件加载到内存的时候,他把代码的数据加载到一块内存中来,其中有很多段代码。加载进来之后默认从0x8048000开始加载,前面是elf头部的一些信息,一般头部的大小会有不同,加载的入口点的位置可能是0x8048300,即程序的实际入口。当启动一个刚加载过可执行文件的进程的时候,开始执行的入口点。文件是一个elf的静态连接文件,链接的时候已经链接好了。从这(0x8048300)开始执行,压栈出栈,从main函数到结束,所有的链接在静态链接时候已经设定好了。正常需要用到共享库或动态链接的时候,情况会复杂一些。
二、可执行程序、共享库和动态加载
使用如下几个文件来举例说明:
shlibexample.h如下
gcc -shared shlibexaple.c -o libshlibexample.so -m32
生成了一个共享库文件
dllibexample.h如下
dllibexample.c如下
编译成.so文件
gcc -shared dllibexample.c -o dllibexample.so -m32
main.c如下
最后编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
$ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
$ ./main
效果如下:
This is a Main program!
Calling SharedLibApi() function of libshlibexample.so!
This is a shared libary!
Calling DynamicalLoadingLibApi() function of libdllibexample.so!
This is a Dynamical Loading libary!
三、使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve
test.c中增加的代码如下:
int Exec(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
printf("This is Child Process!\n");
execlp("/hello","hello",NULL);
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
MenuConfig("fork","Fork a new process",Fork);
MenuConfig("exec","Execute a program",Exec);
ExecuteMenu();
}
然后我们需要改一下Makefile(关于Makefile)的文件内容,使用命令vi Makefile
,添加静态编译hello.c,
gcc -o hello hello.c -m32 -static
cp hello ../rootfs/
find init hello | cpio -o -Hnewc |gzip -9 > ../rootfs.img
在更改Makefile之后,便输入命令make rootfs
来编译了,不过第一次提示我权限不够,我想既然是权限不够,那么就加了个sudo,再编译,果然成功了,如图
接下来要使用gdb开始跟踪调试了,因为我们当前处于menu这个目录下,所以先cd ..
跳到上级目录再使用命令qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
来启动虚拟机,并在最开始的时候停住(-S),-s表示gdb服务器使用默认端口号1234来连接
我们在MenuOS里面输入exec,会弹出两条消息,并停住,即遇到了第一个断点sys_execve,如图
按s进行跟踪,会看到do_execve,如图
我们按c让它继续跑,回跑到第二个断点load_elf_binary处停下来,输入list查看附近代码
继续按c跑起来,停在第三个断点start_thread处,这里有个new_ip,那么这个new_ip到底是指向哪里的?
使用po new_ip
可以看到如下
再水平分割一个控制台出来,使用命令readelf -h hello
,可以看到入口地址跟上面的po new_ip所显示的地址一样
接下里按s,可以看到在进行修改内核堆栈的操作。
可以看到之前的压栈的ip和sp都被改成了新的ip(程序hello的入口点地址)和新的sp,这样在返回到用户态的时候程序就有一个新的可执行上下文环境了。此时再按一下c之后,就结束了,exec的执行就完毕了。
书上内容
- 虚拟文件系统VFS作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。之所以可以使用通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个抽象层。VFS抽象层之所以能衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。
- Unix文件系统四要素:文件、目录、索引节点、安装点。VFS把目录当作文件看待,所以可以对目录执行和文件相同的操作。索引结点:存储访问权限、大小、拥有者、创建时间等信息。超级块:存储文件系统的控制信息。
- VFS四个主要的对象类型(1)超级块对象:代表一个已安装的文件系统。(2)索引节点对象:代表一个文件。(3)目录项对象:代表一个目录项,是路径的一个组成部分。(4)文件对象:代表由进程打开的文件。VFS将目录作为文件来处理,目录项不同于目录。
- 超级块对象:各种文件系统必须实现超级块对象,用于存储特定文件系统信息,通常对应与存放在磁盘特定扇区的文件系统超级块或文件系统控制块。对于sysfs等非磁盘文件系统,超级块存在内存中;索引节点对象:索引节点对象包含内核在操作文件或目录时需要的全部信息;目录项对象(dentry):路径中的每个组成部分都由一个索引节点对象表示。目录项缓存包括三个主要部分:”被使用的”目录项链表、“最近被使用的”双向链表、散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象。文件对象:表示进程已打开的文件,是打开文件在内存中的表示。
- 设备:块设备,随机访问固定大小数据片的设备。字符设备,按字符流方式有序被访问,和块设备的区别在于随机访问。扇区,最小可寻址单元(物理属性)。块,文件系统的最小逻辑可寻址单元。Linux电梯调度算法:相邻合并;驻留时间过长的请求放入队列尾部;保证扇区的磁盘访问顺序;不合适的请求插入位置被放入列队尾部。