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除了这些,还帮用户做了更多,包括把用户烦死。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值