可执行程序是怎么来的?linux
以C语言为例,通过编译器预处理、编译成汇编代码、汇编器编译成目标代码,而后连接成可执行文件,再将可执行程序加载到内存中执行,过程能够经过下图展现(其中预处理已省略):shell
可执行文件的建立--预处理、编译和连接:windows
cd Code vi hello.c gcc –E –o hello.cpp hello.c –m32 //预处理,把include的文件包含进来及宏替换等工做 vi hello.cpp //cpp为预处理的中间文件 gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码 vi hello.s gcc -x assembler -c hello.s -o hello.o -m32 //编译成目标代码 vi hello.o //获得二进制.o文件,ELF格式 gcc -o hello hello.o -m32 //连接成可执行文件hello vi hello //也是二进制文件,ELF格式 gcc -o hello.static hello.o -m32 -static //静态编译,占用内存较大 ls -l
常见的目标文件格式,最古老的目标文件格式是A.out,而后发展成coff,如今咱们经常使用的pe(windows系统运用较多)、elf(linux系统中应用较多)。ELF全称为EXECUTABLE AND LINKABLE FORMAT,便可执行和可连接模式,是一个文件格式的标准。目标文件咱们通常也叫它ABI(应用程序二进制接口),实际上在目标文件里面它已是二进制兼容的格式了,也就是说它这个目标文件已是适应到某一种cpu体系结构上的二进制指令。好比说咱们在一个32位x86编译出来的目标文件连接成ARM平台上的可执行文件确定是不能够的。服务器
elf文件格式中的三种主要目标文件:数据结构
一、一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其余的object文件一块儿来建立一个可执行文件或者是一个共享文件。(主要是.o文件)编辑器
二、一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来建立程序进程映象。函数
三、一个共享object文件保存着代码和合适的数据,用来被下面的两个连接器连接。第一个是链接编辑器[请参看ld(SD_CMD)],能够和其余的可重定位和共享object文件来建立其余的object。第二个是动态连接器,联合一个可执行文件和其余的共享object文件来建立一个进程映象。(主要是.so文件)学习
当建立或增长一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段。如图可执行文件的格式和进程地址空间有一个映射关系:.net
静态连接的ELF可执行文件与进程的地址空间的联系:命令行
当elf文件加载到内存的时候,他把代码的数据加载到一块内存中来,其中有不少段代码。加载进来以后默认从0x8048000开始加载,前面是elf头部的一些信息,通常头部的大小会有不一样,加载的入口点的位置多是0x8048300,即程序的实际入口。当启动一个刚加载过可执行文件的进程的时候,开始执行的入口点。文件是一个elf的静态链接文件,连接的时候已经连接好了。从这(0x8048300)开始执行,压栈出栈,从main函数到结束,全部的连接在静态连接时候已经设定好了。正常须要用到共享库或动态连接的时候,状况会更复杂一点。
装载可执行程序以前,先了解一下可执行程序的执行环境。通常咱们执行一个程序的shell环境,它自己不限制命令行参数的个数,命令行参数的个数受限于命令自身,好比 int main(int argc,char *argv[])
,shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数。命令行参数和环境串都放在用户态的堆栈中。Shell程序->execve -> sys_execve,而后在初始化新程序堆栈时拷贝进去。
动态连接有可执行装载时的动态连接和运行时的动态连接,下面演示了两种动态连接:
共享库shilibexample.c实现SharedLibApi()函数:
#include <stdio.h> #include "shlibexample.h" /* * Shared Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int SharedLibApi() { printf("This is a shared libary!\n"); return SUCCESS; }
shilibexample.h:
#ifndef _SH_LIB_EXAMPLE_H_ #define _SH_LIB_EXAMPLE_H_ #define SUCCESS 0 #define FAILURE (-1) #ifdef __cplusplus extern "C" { #endif /* * Shared Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int SharedLibApi(); #ifdef __cplusplus } #endif #endif /* _SH_LIB_EXAMPLE_H_ */
经过gcc -shared shlibexaple.c -o libshlibexample.so -m32
编译成一个共享库文件,
下面是一样使用 gcc -shared dllibexample.c -o libdllibexample.so -m32
获得动态加载共享库。
其中dellibexample.c实现了DynamicalLoadingLibApi()函数:
#include <stdio.h> #include "dllibexample.h" #define SUCCESS 0 #define FAILURE (-1) /* * Dynamical Loading Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int DynamicalLoadingLibApi() { printf("This is a Dynamical Loading libary!\n"); return SUCCESS; }
dellibexample.h:
#ifndef _DL_LIB_EXAMPLE_H_ #define _DL_LIB_EXAMPLE_H_ #ifdef __cplusplus extern "C" { #endif /* * Dynamical Loading Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int DynamicalLoadingLibApi(); #ifdef __cplusplus } #endif #endif /* _DL_LIB_EXAMPLE_H_ */
main.c()函数:
#include <stdio.h> #include "shlibexample.h" #include <dlfcn.h> /* * Main program * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int main() { printf("This is a Main program!\n"); /* Use Shared Lib */ printf("Calling SharedLibApi() function of libshlibexample.so!\n"); SharedLibApi(); /* Use Dynamical Loading Lib */ void * handle = dlopen("libdllibexample.so",RTLD_NOW); if(handle == NULL) { printf("Open Lib libdllibexample.so Error:%s\n",dlerror()); return FAILURE; } int (*func)(void); char * error; func = dlsym(handle,"DynamicalLoadingLibApi"); if((error = dlerror()) != NULL) { printf("DynamicalLoadingLibApi not found:%s\n",error); return FAILURE; } printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n"); func(); dlclose(handle); return SUCCESS; }
咱们发现main函数中含有include "shlibexample.h" 以及include dlfcn,而没有include dllibexample(动态加载共享库)。当须要调用动态加载共享库时,使用定义在dlfcn.h中的dlopen。
最后经过 gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
接下来编译main()函数,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并无提供dllibexample的相关信息,只是指明了-ldl,而后咱们继续执行:
$ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,不然main找不到依赖的库文件,固然也能够将库文件copy到默认路径下。 $ ./main
打开test.c文件:
能够发现增长了一句,MenuConfig("exec","Execute a program",Exec)
看一下这段代码,和fork()函数相似,增长了一个fork,子进程增长了一个execlp("/hello","hello",NULL); 启动hello,看一下hello.c:
看一下Makefile文件,静态的方式编译了hello.c,并在生成根文件系统时把init 和hello都放在rootfs里面:
输入命令 make rootfs
,在qemu窗口中输入help 执行一下exec:
先cd .. 返回到上一级,qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S启动,水平分割,gdb
而后把符号表文件加载进来,gdb服务器使用默认端口号1234来链接,并经过gdb跟踪调试,设置断点,能够先停在sys_execve而后再设置其余断点:
按c执行:
连续c执行三次,在menuos窗口输入exec,发现执行到 This is child process!
停下:
进入sys_execve系统调用,list列出来,跟踪:
接下来经过s进入sys_execve内部
按c继续执行到load_elf_binary,list查看;再按c执行,执行到start_thread,想知道new_ip到底指向哪里,new_ip是返回到用户态的第一条指令的地址。再水平分割一个控制台出来,使用命令readelf -h hello
,能够看到入口点地址和上面po new_ip所显示的地址同样:
而后咱们继续执行s步骤,能够看到在进行修改内核堆栈的位置,发现原来压栈的ip和sp都被改为了新的ip(程序hello的入口点地址)和新的sp,这样在返回到用户态的时候程序就有一个新的可执行上下文环境。最后按一下c,exec的执行结束:
一、虚拟文件系统(VFS)是Linux内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,容许不一样的文件系统共存。系统中全部的文件系统不但依赖 VFS共存,并且也依靠VFS协同工做。一个实际的文件系统想要被Linux支持,就必须提供一个符合VFS标准的接口,才能与VFS协同工做,跨文件系统操做才能实现
二、VFS借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向Linux中不论是普通的文件仍是目录、设备、套接字等都提供一样的操做界面,如打开、读写、关闭等。只有当实际的文件系统有了控制权时,实际的文件系统才会作出区分,对不一样的文件类型执行不一样的操做。 因此我以为“一切皆是文件”这句话的意思就是,不管是普通的文件,仍是特殊的目录、设备等,VFS都将它们同等看待成文件,Linux都会提供一样的操做界面,这是不一样于实际文件系统的根本。
三、文件是具备完整意义的信息项的系列,在Linux中,除了普通文件,其余诸如目录、设备、套接字等也以文件被对待;目录比如一个文件夹,用来容纳相关文件。由于目录能够包含子目录,因此目录是能够层层嵌套,造成 文件路径; 目录项在一个文件路径中,路径中的每一部分都被称为目录项;如路径/home/source/helloworld.c中,目录 /, home, source和文件 helloworld.c都是一个目录项;索引节点是用于存储文件的元数据的一个数据结构。文件的元数据,也就是文件的相关信息,和文件自己是两个不一样的概念。它包含的是诸如文件的大小、拥有者、建立时间、磁盘位置等和文件相关的信息; 超级块用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。每次一个实际的文件系统被安装时, 内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应,超级块经过其结构中的一个域s_type记录它所属的文件系统类型。
四、进程与超级块、文件、索引结点、目录项的关系
五、两种基本的设备类型,块设备和字符设备,它们的区别在因而否能够随机的访问数据。扇区是设备的最小寻址单元;块是文件系统的最小寻址单元,又称为文件块或者I/O块。缓冲区(buffer)是存放块的区域;缓冲头是存放内核处理数据时须要的一些相关信息的结构描述符(如块属于哪个设备,块对应的是哪一个缓冲区),描述符用buffer_head表示。Linus电梯能执行合并和排序预处理。新提交的请求和原有请求队列中的请求访问的扇区相邻,这种状况能够合并;没有合并的条件,可是有多个请求访问的扇区比较接近,经过将请求排序,可使得磁头线性工做。
VFS把一切都看为文件,提供一样的操做界面,跨文件系统的文件操做基本原理是什么?
好比说,将vfat格式的磁盘上的一个文件a.txt拷贝到ext3格式的磁盘上,命名为b.txt。这包含两个过程,对a.txt进行读操做,对b.txt进行写操做。读写操做前,须要先打开文件。由前面的分析可知,打开文件时,VFS会知道该文件对应的文件系统格式,之后操做该文件时,VFS会调用其对应的实际文件系统的操做方法。因此,VFS调用vfat的读文件方法将 a.txt的数据读入内存;在将a.txt在内存中的数据映射到b.txt对应的内存空间后,VFS调用ext3的写文件方法将b.txt写入磁盘;从而实现了最终的跨文件系统的复制操做。
参考网址:Linux的虚拟文件系统