linux程序的命令行参数

程序执行的时候需要命令行参数,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,

??????????????????????? &regs);

…//我们今天的问题到此为止,以下省略

}

继续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) < 0)

???????????????? goto out_mm;

???????? bprm->envc = count(envp, MAX_ARG_STRINGS);

???????? if ((retval = bprm->envc) < 0)

???????????????? 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 < 0)

???????????????? goto out;

???????? env_p = bprm->p;

???????? retval = copy_strings(bprm->argc, argv, bprm);//拷贝argc

???????? if (retval < 0)

???????????????? 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 < MAX_ARG_PAGES ; i++) {? //这个循环将bprm的page映射到了新的地址空间

???????????????? 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程序的命令行参数

相关文章:

你感兴趣的文章:

标签云: