问题的由来
我们知道,普通的C程序可以从命令行上接收参数,也可以使用、设置SEHLL环境变量(getenv,setenv),
/*一个简单的C例子test.c*/
#include"stdio.h"
externchar**environ;/*C库定义的全局变量,环境变量字符串数组的起始地址*/
intmain(intargc,char*argv[])
{
inti;
char**p=environ;
for(i=0;i
printf("argv[%d]:%s\n",i,argv[i]);
printf("\n\nEnviromentVaribles\n\n");
while(*p!=NULL)
printf("%s\n",*(p++));
return0;
}
编译后在命令行上运行
:
./testarg1arg2.
可以将运行结果与env输出结果比较
执行普通的C程序时,因为有SHELL的存在,命令行参数和环境变量可以由SHELL传给C程序,这一点似乎容易理解。然而,对于系统启动时运行的第一个用户程序init来说情况变得有些特别。init不是由用户在命令行上启动的(这时候根被还没有SHELL),而是由内核启动的,init程序里也使用了命令行参数和环境变量。
/*Busybox-1.19.3/init/init.c*/
intinit_main(intargcUNUSED_PARAM,char**argv)
{
if(argv[1]&&strcmp(argv[1],"-q")==0){
returnkill(1,SIGHUP);
}
//……
console_init();
//……
}
staticvoidconsole_init(void)
{
//.......
s=getenv("CONSOLE");
s=getenv("TERM");
//......
}
从上面的代码片段里可以看到init程序和普通应用程序一样,也使用到了命令行参数和环境变量,既然这个时候还没有SHELL,那么init使用的命令行参数和环境变量保存在何处、从何而来?
保存在何处
Linux采用了虚拟内存技术,进程里使用的均是虚拟地址,内核通过为进程建立不同的页表,可以将两个相互独立的进程的相同的虚拟地址映射到不同的物理地址上,因此,Linux下所有可执行文件运行时在地址空间里的映像布局结构是一样的,这简化了系统设计。
下图是C程序执行时,在进程的用户空间里的映像布局分布(没有画出C库及动态连接器映射的部分)。
程序
#include
#include
#include
externchar**environ;
intin_data_segment=2;
intin_bss_segment;
intmain(intargc,char*argv[])
{
charcmd[32];
charline[1024];
FILE*fp;
printf("addrofargv=%x\n\n",(void*)argv);
printf("addrofenviron=%x\n",(void*)environ);
printf("addrofin_data_segment=%x\n",(void*)&in_data_segment);
printf("addrofin_bss_segment=%x\n\n",(void*)&in_bss_segment);
if((fp=fopen("/proc/self/maps","r"))==NULL)
{
printf("open/proc/self/mapserror\n");
return0;
}
while(fgets(line,1024,fp)!=NULL)
fputs(line,stdout);
return0;
}
程序运行结果
addrofargv=bfccb4f4
addrofenviron=bfccb4fc
addrofin_data_segment=80498e8
addrofin_bss_segment=804992c
08048000-08049000r-xp0000000075:00106839/home/temp/memlayout/*text段*/
08049000-0804a000rw-p0000000075:00106839/home/temp/memlayout /*text、data、bss段*/
0804a000-0806b000rw-p0804a00000:000 [heap]
b7f2f000-b809d000r-xp0000000075:00204907 /lib/libc-2.9.so
b809d000-b809f000r--p0016e00075:00204907/lib/libc-2.9.so
b809f000-b80a0000rw-p0017000075:00204907/lib/libc-2.9.so
b80a0000-b80a4000rw-pb80a000000:000
b80a9000-b80ab000rw-pb80a900000:000
b80ab000-b80cb000r-xp0000000075:00204900/lib/ld-2.9.so
b80cb000-b80cc000rw-pb80cb00000:000
b80cc000-b80cd000r--p0002000075:00204900/lib/ld-2.9.so
b80cd000-b80ce000rw-p0002100075:00204900/lib/ld-2.9.so
bfcb8000-bfccd000rw-pbffeb00000:000[stack]/*命令行参数、环境变量、stack*/
ffffe000-fffff000r-xp0000000000:000[vdso]/*这部分属于内核空间*/
从上面的内存分布可以看出命令行参数和环境变量保存在进程地址空间的末端,下面的问题是参数和变量是从何而来,又是如何映射到了指定的位置。
从何而来
内核在完成一系列的软硬件初始化、成功挂载根文件系统之后,内核尝试在根文件系统里寻找init程序并加载运行,从这里进入到用户空间。
run_init_process("/sbin/init");
staticvoidrun_init_process(char*init_filename)
{
argv_init[0]=init_filename;
kernel_execve(init_filename,argv_init,envp_init);
}
kernel_execve是在内核空间里执行用户空间程序的接口,从调用形式上可以猜测,argv_init、envp_init就是传递给init的命令行参数和环境变量参数。
/*体系结构相关,arch/arm/kernel/sys_arm.c*/
intkernel_execve(constchar*filename,char*constargv[],char*constenvp[])
{
structpt_regsregs;
intret;
memset(&s,0,sizeof(structpt_regs));
ret=do_execve((char*)filename,(char__user*__user*)argv,(char__user*__user*)envp,&s);
//do_execve调用成功的话kernel_execve就不会再返回内核空间而是到用户空//间去了,这通过操作栈来实现
}
intdo_execve(char*filename,char__user*__user*argv,char__user*__user*envp,structpt_regs*regs)
{
structlinux_binprm*bprm;
structfile*file;
//......
bprm=kzalloc(sizeof(*bprm),GFP_KERNEL);
file=open_exec(filename);
bprm->file=file;
retval=bprm_mm_init(bprm);
bprm->argc=count(argv,MAX_ARG_STRINGS);
bprm->envc=count(envp,MAX_ARG_STRINGS);
retval=copy_strings(bprm->envc,envp,bprm);
retval=copy_strings(bprm->argc,argv,bprm);
retval=search_binary_handler(bprm,regs);
}
copy_strings里会申请物理内存,将字符串拷贝到新申请的内存里。
search_binary_handler会根据可执行文件格式搜索系统已注册的的处理函数处理。
Linux下标准的可执行文件格式是ELF,除此之外,Linux还支持多种可执行文件格式。Linux用structlinux_binfmt数据结构来描述可执行文件格式,系统在启动时,由子系统调用register_binfmt来注册所支持的可执行文件格式,所有注册的structlinux_binfmt在内核里形成链表。内核在启动程序时,分析可执行文件的信息,在这个链表里搜索匹配的项。
structlinux_binfmt{
structlist_headlh;
structmodule*module;
int(*load_binary)(structlinux_binprm*,structpt_regs*regs);
int(*load_shlib)(structfile*);
int(*core_dump)(structcoredump_params*cprm);
unsignedlongmin_coredump; /*minimaldumpsize*/
inthasvdso;
};
对应到ELF格式,这里的load_binary的值是load_elf_binary
/*fs/binfmt_elf.c*/
staticintload_elf_binary(structlinux_binprm*bprm,structpt_regs*regs)
{
/*load_elf_binary函数会根据bprm的内容以及分析将要执行的elf格式的可执行文件的信息,将text、data、bss段映射到进程地址空间的相应位置,调用一个名为setup_arg_pages的函数,将之前保存到新申请的物理内存页的命令行参数和环境变量映射到进程地址空间的指定位置,构建出图一所示的内存分布,建立起运行环境,最后跳转到程序的入口处开始执行*/
start_thread(regs,elf_entry,bprm->p);
}
从上面的分析可以看出,传递给init的参数和环境变量的来自于argv_init和envp_init。定义如下:
/*init/main.c*/
staticchar*argv_init[MAX_INIT_ARGS+2]={"init",NULL,};
char*envp_init[MAX_INIT_ENVS+2]={"HOME=/","TERM=linux",NULL,};
这是两个内核全局变量,那么应该就处在内核空间,通过copy_strings、setup_arg_pages的处理被映射到用户空间的指定位置,最终完成从内核到用户空间的传递。
除了定义时的默认值,bootloader启动内核时,还可以通过内核启动参数传递新的值给这两个变量。内核处理bootloader传递过来的启动参数时,分三类,
1.内核子系统通过__setup注册的参数处理函数将会处理掉它能够识别的参数,如
__setup("init=",init_setup);启动参数里“init=”将被init_setup函数使用掉。
2.剩下的形如”foo=bar”形式的参数解析为环境变量,保存到envp_init中。
3.其余的参数保存到argv_init中。
小结
Bootloader引导Linux时,通过内核启动参数设置argv_init和envp_init的值
内核调用kernel_execv启动init程序,将argv_init和envp_init作为调用参数传递进去
do_execve里申请新的物理内存页,将argv_init和envp_init新申请到物理内存处,通过setup_arg_pages将物理内存页映射到进程地址空间的指定位置处
init程序运行起来以后,从进程地址空间的指定位置处获取命令行参数和环境变量
普通应用程序的情形
Linux下面一个应用程序运行起来分两个步骤。首先通过fork系统调用创建出一个新的进程,这个新创建的进程会继承父进程的地址空间里的一些内容。然后,在新创建的子进程中通过execve加载将要运行的可执行文件,加载的过程里会释放从父进程那里继承而来的旧的地址空间,并根据可执行文件的解析结果创建新的地址空间,自此,新的应用程序就运行起来了。execve函数原型:
intexecve(constchar*pathname,char*constargv[],char*constenvp[]);
execve最终由内核系统调用处理函数:sys_execv处理。
/*体系结构相关,arch/arm/kernel/sys_arm.c*/
intsys_execve(char__user*filenamei,char__user*__user*argv,char__user*__user*envp,structpt_regs*regs)
{
interror;
char*filename;
//......
filename=getname(filenamei);
error=PTR_ERR(filename);
if(IS_ERR(filename))
gotoout;
//......
error=do_execve(filename,argv,envp,regs);
//.....
putname(filename);
out:
returnerror;
}
可以看到,通过execve->sys_execve->do_execve执行普通用户程序和内核启动init程序,最终都到了do_execve,与init不同的是,argv和envp参数来源不一样。显然,普通用户程序的argv和envp参数只能是调用execve进程来指定。可以猜测,通过SHELL启动的用户程序,将由SHELL来准备好这两个参数。
/*以下例子来自于《UNIX环境高级编程》Figure8.16*/
#include
#include
char*env_init[]={"USER=unkown","PATH=tmp",NULL};
intmain(void)
{
pid_tpid;
if((pid=fork())
{
printf("forkerror\n");
return-1;
}
elseif(pid==0)/*child*/
{
if(execle("/home/temp/exec/echo","echo","arg1","arg2",(char*)0,env_init)
{
printf("execleerror\n");
return-1;
}
}
if(waitpid(pid,NULL,0)
{
printf("waiterror");
return-1;
}
printf("testdone!\n");
return0;
}
/*/home/temp/exec/echo*/
#include
externchar**environ;
intmain(intargc,char*argv[])
{
inti;
char**ptr;
for(i=0;i
printf("argv[%d]:%s\n",i,argv[i]);
for(ptr=environ;*ptr!=NULL;ptr++)
printf("%s\n",*ptr);
}
运行结果:
argv[0]:echo
argv[1]:arg1
argv[2]:arg2
USER=unkown
PATH=tmp
testdone!