- 沈鑫 + 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1、一个可执行程序的得到过程:编译器预处理->编译成汇编代码->汇编器编译成目标代码->链接成可执行文件->由操作系统将可执行文件加载到操作系统中进行执行
其中目标代码是不可执行的,因为他可能还缺少执行所需要的动态库。
shiyanlou:~/ $ cd Code
shiyanlou:Code/ $ vi hello.c
shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c -m32 //对文件进行预处理,对宏定义进行替换
shiyanlou:Code/ $ vi hello.cpp
shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码
shiyanlou:Code/ $ vi hello.s
shiyanlou:Code/ $ gcc -x assembler -c hello.s -o hello.o -m32 //把汇编代码编译成目标代码
shiyanlou:Code/ $ vi hello.o
shiyanlou:Code/ $ gcc -o hello hello.o -m32 //把目标文件链接成可执行文件,这样使用的是共享库
shiyanlou:Code/ $ vi hello //hello.o和hello都是ELF格式的文件
shiyanlou:Code/ $ gcc -o hello.static hello.o -m32 -static //这是静态编译的方法
shiyanlou:Code/ $ ls -l
-rwxrwxr-x 1 shiyanlou shiyanlou 7292 3\u6708 23 09:39 hello
-rw-rw-r-- 1 shiyanlou shiyanlou 64 3\u6708 23 09:30 hello.c
-rw-rw-r-- 1 shiyanlou shiyanlou 17302 3\u6708 23 09:35 hello.cpp
-rw-rw-r-- 1 shiyanlou shiyanlou 1020 3\u6708 23 09:38 hello.o
-rw-rw-r-- 1 shiyanlou shiyanlou 470 3\u6708 23 09:35 hello.s
-rwxrwxr-x 1 shiyanlou shiyanlou 733254 3\u6708 23 09:41 hello.static
ELF文件包含三种格式,他们分别是:可重定位文件、可执行文件、共享object文件。ELF文件的头部,保存了路线图,描述了该文件的组织情况。
程序头表告诉系统如何来创建一个进程的内存映像。
section头表包含了描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字、大小等信息。
例如main.c文件,可以通过readlf -h main
来查看可执行文件的头部。
2、可执行程序的执行环境
shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身。
例如,int main(int argc, char *argv[])
又如,int main(int argc, char *argv[], char *envp[])
shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数。
库函数exec*都是execve的封装例程,下面就是一个execve的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(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 */
execlp("/bin/ls","ls",NULL);
}
else
{
/* parent process */
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!");
exit(0);
}
当创建一个子进程的时候(fork)完全复制的是父进程;当调用exec*的时候,要加载的进程环境将原来的进程环境给覆盖掉了,原来的用户态堆栈也被清空了。当创建一个新的用户态堆栈的时候,实际上是将命令行参数和环境变量内容通过指针传递给系统调用的内核处理函数。
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
然后内核处理函数在创建新的用户态堆栈的时候会把这些命令行参数和环境变量拷贝到新的用户态堆栈,来对进行初始化。
2.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函数中进行调用,但是运行时动态链接则至少需要用到dlopen和dlsym。运行结果如下:
值得注意的是新进程的返回地址是用户态的第一条指令的地址。
总结:
当我们将一个程序经过一些列处理后变为可执行程序时(以main.c为例),我们要执行这个./main时,shell命令调用exec*系统调用。而exec*中创建了一个新的进程,并将原来进程的用户态堆栈给覆盖掉了。其实后来的进程就已经是新的进程,而不是原来的进程了。