程序执行的时候需要命令行参数,linux中更是这样,随便在shell输入/bin/XX --help后列举出来的参数让你头晕眼花,可是这些参数是怎么进入程序的呢,我们知道程序执行的时候一般从main开始,而mian有两个参数,一个是 argc代表参数的个数,一个是argv代表具体字符串类型的参数,这是我们所看到的,我们都知道函数的参数都在堆栈中,在调用函数前,主调函数应该将参数压入堆栈后再调用被调函数,那么是谁调用的main函数呢?又是谁将main的参数压入堆栈的呢?
关于第一个问题,是谁调用的main函数,我就不多说了,因为网上已经有了一篇叫做《before main》的文章了,写得非常好,可以搜索一下,读了此文你会明白实际上用户进程的开始函数并不是main,在main之前还有很多工作要做,但是如果说 是XX调用了main,那么就是XX压入了参数,我们很多人喜欢纠着一个问题一直到底,那我们就较较真儿,又是谁将参数给了XX呢?我们开始一个程序的时 候要调用exec系列函数,比如execve,我们看看execve的声明:
int execve(const char *filename, char *const argv[],char *const envp[]);
我 们看一下这第二个和第三个参数实际上就是main的参数(main的第一个参数argc是由这些参数算出来的),而调用execve的时候还是原来的进 程,新的进程还只是一个filename,具体能否执行还有待商榷呢,新进程根本没有映射进用户空间,这时这些参数是怎么传递给新的进程的呢?我们于是就来正式解答第二个问题:又是谁将main的参数压入堆栈的呢?
研究linux有个好的不得了的资源就是内核,当你遇到任何棘手的问题都可以从内核得到解答,当然今天我们的问题并不算棘手!我们还是看看sys_ececve是怎么做的:
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
®s);
...//我们今天的问题到此为止,以下省略
}
继续do_execve
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
struct linux_binprm *bprm;
...
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
...
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
...
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc)
goto out_mm;
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc)
goto out_mm;
...
retval = copy_strings_kernel(1, &bprm->filename, bprm);//拷贝文件名称
...
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);//拷贝envc
if (retval
goto out;
env_p = bprm->p;
retval = copy_strings(bprm->argc, argv, bprm);//拷贝argc
if (retval
goto out;
bprm->argv_len = env_p - bprm->p;
retval = search_binary_handler(bprm,regs);
...
}
我们看到argc是怎么算出来的:
bprm->argc = count(argv, MAX_ARG_STRINGS);
它实际上就是算出了参数的个数,下面最重要的就是copy_strings函数了,这个函数的意义就是将参数拷贝到一个内核的页面当中并设置为bprm的一个字段,我们先看看bprm结构:
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
struct mm_struct *mm;
unsigned long p; /* current top of mem */
...
};
这里的page就是存放参数的,copy_strings做的就是将参数从调用execve的进程先拷贝到bprm的page中,具体实现就不列出了,因为最新的版本的linux_binprm 中已经添加了vma来做这件事,但是本质山还是一样,那么为何要经这么一二传手呢?进程调用完exec不还是这个进程吗?为何还要拷贝呢?其实这个问题的 答案很简单,就是虽然还是这个进程,但是他的地址空间却从新设置了,原来的地址空间被他release了,可以看一下我前面的文章或者自己看内核源码。既 然地址空间改变了,那么就必须把新地址空间要用到的东西先转移到一个地方,然后新的地址空间加载以后再把它从这个地方拷贝到新地址空间,那么拷贝到哪里比较安全呢?当然是内核了,呵呵。
现在我们知道了,bprm的page就是存放参数的了,这个结构还有一个重要的字段就是p,一个unsigned long,其实是记录参数大小的,它被初始化为:PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *),在copy参数的函数copy_strings里这个字段被减小,减小多少呢?减小的就是参数的总长度,比如有3个参数,第一个是“aa”,第二个 是“bb”,第三个是“cc”,那么它就被减小6,一会再说。现在问题就变为什么时候将bprm的page拷贝到新的地址空间了,这就涉及到elf文件的 加载了,在elf的加载函数里设置了新地址空间的堆栈,于是我们找到了setup_arg_pages:
int setup_arg_pages(struct linux_binprm *bprm, int executable_stack)
{
unsigned long stack_base;
struct vm_area_struct *mpnt;
struct mm_struct *mm = current->mm;
int i;
long arg_size;
stack_base = STACK_TOP - MAX_ARG_PAGES * PAGE_SIZE;//一般的堆栈向下增长,那么参数最大能增长到的地方就是stack_base,因为MAX_ARG_PAGES限制了参数的总页数,不过这个值已经够大了。
mm->arg_start = bprm->p + stack_base;//将参数的起始地址调整为实际的起始地址,根据就是bprm的字段p,在拷贝参数进内核的时候已经将p置为了PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *)减去参数的总大小,那么这里的mm->arg_start正好是参数的从底地址到高地址的起始地址。
arg_size = STACK_TOP - (PAGE_MASK & (unsigned long) mm->arg_start);//参数的大小
bprm->p += stack_base;
if (bprm->loader)
bprm->loader += stack_base;
bprm->exec += stack_base;
mpnt = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);//为新的进程分配的第一个vma,主要就是设置参数,实际上最终就是main函数的参数
if (!mpnt)
return -ENOMEM;
if (security_vm_enough_memory(arg_size >> PAGE_SHIFT)) {
kmem_cache_free(vm_area_cachep, mpnt);
return -ENOMEM;
}
memset(mpnt, 0, sizeof(*mpnt));
down_write(&mm->mmap_sem);
{
mpnt->vm_mm = mm;
mpnt->vm_start = PAGE_MASK & (unsigned long) bprm->p;//这个在纸上比划两下子就清楚了
mpnt->vm_end = STACK_TOP;
if (unlikely(executable_stack == EXSTACK_ENABLE_X))
mpnt->vm_flags = VM_STACK_FLAGS | VM_EXEC;
else if (executable_stack == EXSTACK_DISABLE_X)
mpnt->vm_flags = VM_STACK_FLAGS & ~VM_EXEC;
else
mpnt->vm_flags = VM_STACK_FLAGS;
mpnt->vm_flags |= mm->def_flags;
mpnt->vm_page_prot = protection_map[mpnt->vm_flags & 0x7];
insert_vm_struct(mm, mpnt);
mm->stack_vm = mm->total_vm = vma_pages(mpnt);
}
for (i = 0 ; i
struct page *page = bprm->page[i];
if (page) {
bprm->page[i] = NULL;
install_arg_page(mpnt, page, stack_base);//具体映射,就是建立页表映射
}
stack_base += PAGE_SIZE;
}
up_write(&mm->mmap_sem);
return 0;
}
上 面的函数映射了参数到新的地址空间,如果你认为一切到此结束了,那么就大错特错了,看看main的参数还有个argc,argc在do_execve中已 经被计算出来了,难道还要libc库再算一遍吗?实际上argc也要压入参数堆栈,就是在load_elf_binary的最后调用了 create_elf_tables函数,这个函数作了这个工作:
static void create_elf_tables(struct linux_binprm *bprm, struct elfhdr * exec, int interp_aout, unsigned long load_addr, unsigned long interp_load_addr)
{
unsigned long p = bprm->p;
int argc = bprm->argc;
int envc = bprm->envc;
elf_addr_t __user *argv;
elf_addr_t __user *envp;
...
if (k_platform) {
size_t len = strlen(k_platform) + 1;
u_platform = (elf_addr_t __user *)STACK_ALLOC(p, len);
__copy_to_user(u_platform, k_platform, len);
}
elf_info = (elf_addr_t *) current->mm->saved_auxv;
#define NEW_AUX_ENT(id, val) /
do { elf_info[ei_index++] = id; elf_info[ei_index++] = val; } while (0)
...
ei_index += 2;
sp = STACK_ADD(p, ei_index);
items = (argc + 1) + (envc + 1); //items就是参数的数量
if (interp_aout) {
items += 3;
} else {
items += 1; //对于elf就再加一个argc就可以了
}
bprm->p = STACK_ROUND(sp, items);
sp = (elf_addr_t __user *)bprm->p;
__put_user(argc, sp++); //终于压入argc了
...//不考虑a.out了
} else {
argv = sp;
envp = argv + argc + 1;
}
p = current->mm->arg_start;
while (argc-- > 0) { //一次压入argv参数的指针
size_t len;
__put_user((elf_addr_t)p, argv++);
len = strnlen_user((void __user *)p, PAGE_SIZE*MAX_ARG_PAGES);
if (!len || len > PAGE_SIZE*MAX_ARG_PAGES)
return;
p += len;
}
__put_user(0, argv);
current->mm->arg_end = current->mm->env_start = p;
while (envc-- > 0) {
size_t len;
__put_user((elf_addr_t)p, envp++);
len = strnlen_user((void __user *)p, PAGE_SIZE*MAX_ARG_PAGES);
if (!len || len > PAGE_SIZE*MAX_ARG_PAGES)
return;
p += len;
}
__put_user(0, envp);
current->mm->env_end = p;
sp = (elf_addr_t __user *)envp + 1;
copy_to_user(sp, elf_info, ei_index * sizeof(elf_addr_t));
}
要 想彻底明白这个机制,其实明白main的参数结构就可以了,看看第二个参数char *argv[],实际上就是个指针的指针了,argv指向一个数组的头指针,而此数组的元素是字符串,copy_strings拷贝的是各个字符串的内容,在当时由于新的地址空间还未就位因此根本谈不上指针,因为指针其实就是地址空间的一个地址,后来到了create_elf_tables的时候,起码参数相关的vma已经就位了,因此地址信息就确定了,因此只有在这里推入各个参数的指针信息,而这些指针指向的就是copy_strings拷贝的内容,相应的指针值是通过参数vma的内部地址和参数数量算出来的。
这就是堆栈的好处,帮助一切函数调用传递参数,包括main函数(rom中无法调用函数就是因为rom不可写,而操作堆栈必须写内存)。linux将一切 策略留给用户,仅仅帮助用户推入了一系列main函数的参数,不光linux,windows也是这样,不过windows除了这些,还帮用户做了更多,包括把用户烦死。
分享到:
相关推荐
编写一个程序,对用户由命令行参数传入的一个名称进行判断。如果是一个文件名,则输出该文件相关属性 ( 文件名、路径、绝对路径、是否可读、是否可写和文件的长度等 ) 。如果是一个目录,则输出该目录下的文件及子...
linux实验2,有需要更多的请联系我,这是我的自己的实验报告,想换点分啊
几乎所有的GNU/Linux程序都遵循一些同样的命令行解释习惯,程序的参数通常 分为了两大类:选项(option)或者一些标志(flag)、其他参数。选项(option)主要是提供给程序一些运行上的选择,而其他参数则通常是提 供给...
之所以用到命令行参数,关键在于shell脚本需要与运行脚本的人员进行交互。bash shell提供了命令行参数添加在命令后面的数据值)、命令行选项修改命令行为的单字符值)和直接读取键盘输入。 1、命令行参数向shell脚本...
“mytime”命令通过命令行参数接受要运行的程序,创建一个独立的进程来运行该程序,并记录程序运行的时间。 在Linux下实现: • 使用fork()/execv()来创建进程运行程序 • 使用wait()等待新创建的进程结束 • ...
1.在linux中实现一个命令执行程序doit,它执行命令行参数中的命令,之后统计 1)命令执行占用的CPU时间(包括用户态和系统态时间,以毫秒为单位), 2)命令执行的时间, 3)进程被抢占的次数, 4)进程主动放弃CPU的...
Gooey 论证了 argparse 命令行解析库期望的参数,并把它们作为 GUI 形式呈现给用户,所有选项都使用适当的控件(例如多选项参数的下拉列表等)进行标记和显示。 假设你已经在使用 argparse,只需要很少的附加编码...
命令行界面的程序,通常都需要输入命令行参数帮助程序执行。假定有一个可执 行程序名为test。那么运行该程序的的命令行如下(Linux下): test 带命令行参数是同一行中的附加项: ./test I "Like IT" !...
find是在磁盘中查找满足给定标准的文件和目录的应用程序。默认情况下,它从当前目录开始,向下扫描所有子目录。Find基于各种不同的文件属性来进行查询,而且可以...同样可以搜索基于组用户的文件,使用“-group”参数。
有关几个shell脚本的编写,如:编写一个shell脚本程序,它带一个命令行参数,这个参数是一个文件。如果这个文件是一个普通文件,则打印文件所有者的名字和最后的修改日期...加入了自己的注释理解
"mytime”命令通过命令行参数接受要运行的程序,创建一个独立的进程来运行该程序,并记录程序运行的时间。 在Windows下实现: 使用CreateProcess()来创建进程 使用WaitForSingleObject()在"mytime”命令和新创建的...
Linux下写的 socket 文件传输送程序 服务端和客户端完美统一! 命令行参数分析的完美方式!
参数计数.sh超时和输入计数.sh处理带值的选项.sh处理简单选项.sh从文件中读取数据.sh读取参数.sh读取程序名.sh读取多个命令行参数,sh分离参数和选项.sh获取用户输入.sh使用getopts.sh使用getopts处理选项和参数.sh...
在脚本中,可以使用echo命令来输出这些变量的值,以便了解命令行参数的内容。 循环语句 在Linux Shell脚本编程中,循环语句是非常重要的控制结构之一。until循环语句是一种特殊的循环语句,用于实现循环操作直到...
本文实例讲述了python命令行参数用法。分享给大家供大家参考,具体如下: 在命令行下执行某些命令的时候,通常会在一个命令后面带上一些参数,这些参数会传递到程序里,进行处理,然后返回结果,在linux 下很多命令...
当应用程序已经在运行, 再次运行该应用程序时,通常只是把该应用程序的窗口提到前面来,把新的命令行参数传递给第一个运行实例,而第二个实例退出。这在传统的单进程多线程的手机 平台中,实现是简单而直接的,而在...
该程序主要完成Linux命令行G++环境下编译并执行二进制加法的功能。该程序支持命令行直接向main函数传递参数。
#命令只能以命令行的形式运行,命令格式中包括命令字、命令选项和命令参数 #应用程序可以是以命令行的形式运行,也可以是字符界面或图形界面的窗口程序,形式比较多样 9.2系统应用程序与第三方应用程序比较 #系统...
(1)Shell应该解析命令行参数指针数组argv[const]。使用Linux的系统调用fork()、wait()、和execv()等完成。 (2)对用户编写的Shell增加后台运行功能。即用户可以使用“&”作为一个命令,表示该命令在后台启动。...