`
xitong
  • 浏览: 6229411 次
文章分类
社区版块
存档分类
最新评论

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)

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

分享到:
评论

相关推荐

    File 类:对用户由命令行参数传入的一个名称进行判断

    编写一个程序,对用户由命令行参数传入的一个名称进行判断。如果是一个文件名,则输出该文件相关属性 ( 文件名、路径、绝对路径、是否可读、是否可写和文件的长度等 ) 。如果是一个目录,则输出该目录下的文件及子...

    linux实验6 给偷懒的兄弟们

    linux实验2,有需要更多的请联系我,这是我的自己的实验报告,想换点分啊

    Linux操作系统的程序命令行处理分析

    几乎所有的GNU/Linux程序都遵循一些同样的命令行解释习惯,程序的参数通常 分为了两大类:选项(option)或者一些标志(flag)、其他参数。选项(option)主要是提供给程序一些运行上的选择,而其他参数则通常是提 供给...

    shell脚本命令行参数简介

    之所以用到命令行参数,关键在于shell脚本需要与运行脚本的人员进行交互。bash shell提供了命令行参数添加在命令后面的数据值)、命令行选项修改命令行为的单字符值)和直接读取键盘输入。 1、命令行参数向shell脚本...

    操作系统实验,进程控制mytime

    “mytime”命令通过命令行参数接受要运行的程序,创建一个独立的进程来运行该程序,并记录程序运行的时间。 在Linux下实现: • 使用fork()/execv()来创建进程运行程序 • 使用wait()等待新创建的进程结束 • ...

    已运行通过的代码

    1.在linux中实现一个命令执行程序doit,它执行命令行参数中的命令,之后统计 1)命令执行占用的CPU时间(包括用户态和系统态时间,以毫秒为单位), 2)命令执行的时间, 3)进程被抢占的次数, 4)进程主动放弃CPU的...

    命令行程序转GUI程序Gooey.zip

    Gooey 论证了 argparse 命令行解析库期望的参数,并把它们作为 GUI 形式呈现给用户,所有选项都使用适当的控件(例如多选项参数的下拉列表等)进行标记和显示。 假设你已经在使用 argparse,只需要很少的附加编码...

    C程序中main的参数

    命令行界面的程序,通常都需要输入命令行参数帮助程序执行。假定有一个可执 行程序名为test。那么运行该程序的的命令行如下(Linux下):    test  带命令行参数是同一行中的附加项:    ./test I "Like IT" !...

    Linux系统命令行Find工具使用小技巧

    find是在磁盘中查找满足给定标准的文件和目录的应用程序。默认情况下,它从当前目录开始,向下扫描所有子目录。Find基于各种不同的文件属性来进行查询,而且可以...同样可以搜索基于组用户的文件,使用“-group”参数。

    Linux shell 脚本 markdown文本编辑

    有关几个shell脚本的编写,如:编写一个shell脚本程序,它带一个命令行参数,这个参数是一个文件。如果这个文件是一个普通文件,则打印文件所有者的名字和最后的修改日期...加入了自己的注释理解

    windows进程控制mttime

    "mytime”命令通过命令行参数接受要运行的程序,创建一个独立的进程来运行该程序,并记录程序运行的时间。 在Windows下实现: 使用CreateProcess()来创建进程 使用WaitForSingleObject()在"mytime”命令和新创建的...

    brt.rar_socket linux_socket命令行_socket文件_socket文件传输_文件传输

    Linux下写的 socket 文件传输送程序 服务端和客户端完美统一! 命令行参数分析的完美方式!

    Linux命令行与shell脚本编程 - 处理用户输入

    参数计数.sh超时和输入计数.sh处理带值的选项.sh处理简单选项.sh从文件中读取数据.sh读取参数.sh读取程序名.sh读取多个命令行参数,sh分离参数和选项.sh获取用户输入.sh使用getopts.sh使用getopts处理选项和参数.sh...

    LinuxShell脚本编程实例.pdf

    在脚本中,可以使用echo命令来输出这些变量的值,以便了解命令行参数的内容。 循环语句 在Linux Shell脚本编程中,循环语句是非常重要的控制结构之一。until循环语句是一种特殊的循环语句,用于实现循环操作直到...

    python命令行参数用法实例分析

    本文实例讲述了python命令行参数用法。分享给大家供大家参考,具体如下: 在命令行下执行某些命令的时候,通常会在一个命令后面带上一些参数,这些参数会传递到程序里,进行处理,然后返回结果,在linux 下很多命令...

    Linux手机平台让应用程序单实例运行

    当应用程序已经在运行, 再次运行该应用程序时,通常只是把该应用程序的窗口提到前面来,把新的命令行参数传递给第一个运行实例,而第二个实例退出。这在传统的单进程多线程的手机 平台中,实现是简单而直接的,而在...

    通过命令行向main函数传递参数的二进制加法器

    该程序主要完成Linux命令行G++环境下编译并执行二进制加法的功能。该程序支持命令行直接向main函数传递参数。

    第9章linux应用程序安装与管理

    #命令只能以命令行的形式运行,命令格式中包括命令字、命令选项和命令参数 #应用程序可以是以命令行的形式运行,也可以是字符界面或图形界面的窗口程序,形式比较多样 9.2系统应用程序与第三方应用程序比较 #系统...

    C语言实现Linux内核Shell程序

    (1)Shell应该解析命令行参数指针数组argv[const]。使用Linux的系统调用fork()、wait()、和execv()等完成。 (2)对用户编写的Shell增加后台运行功能。即用户可以使用“&”作为一个命令,表示该命令在后台启动。...

Global site tag (gtag.js) - Google Analytics