greenbug的笔记 https://passport2.21ic.com/?419371 [收藏] [复制] [RSS]

日志

uClinux下用户程序的执行

已有 898 次阅读2007-10-19 09:48 |个人分类:linux&uClinux|系统分类:ARM| uClinux, 用户程序, 执行

uClinux下用户程序的执行


    之所以从用户程序谈起,是因为我们平常接触最多的还是应用程序。
从应用程序引出到操作系统我觉得比较自然。下面就从一个简单例子介
绍一个程序如何在操作系统中运行。


    假如有个c程序:
    int main(int argc, char **argv[])
    {
        printf("hello world!
");
        return 0;
    }


    这是一个最简单不过的程序了,一般一个C语言程序,都从main开始
执行。那么,main函数是不是与其他函数有所区别,地位有些特殊呢?
不是的。main函数和其他函数地位一样。其实,我们完全可以做到让一个
c程序从任何地方开始执行。比如linux,它就没有main函数,大家都知道,
系统执行过启动的一段汇编后,就会跳转到位于init/main.c中的
start_kernel中开始执行。


    那么为什么用户程序都要从main函数执行呢?这就是用户C库的原因。
一般用户用c语言开发时会调用一些库函数,编译成obj文件后,在链接过
程中把库函数的二进制代码链接进入程序,最后形成二进制可执行文件。
链接过程中,链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s,
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号,比如foo。而你的程序里面既有main,又
有foo,则这种情况下,程序就先从foo开始执行。所以,main函数和其他
函数一样,并没有特殊地位。


    下面谈谈在uClinux中,main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单,基本上是平铺的,所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单,我还是用flat格式举例。这里暂不分析flat文件格式,我们把注
意力放到参数传递上。uClinux开发用户程序,首先当然是编码,然后编译,
编译生成的文件是elf格式的,所以要用工具elf2flt将elf文件转换成flat,
假设这个工作已经完成。


    我们在uclinux的shell下执行一个文件foo x y,foo是程序名,x, y是
参数。学过C语言的都知道,x,y作为参数会传递给main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候,操作系统会调用
程中把库函数的二进制代码链接进入程序,最后形成二进制可执行文件。
链接过程中,链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s,
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号,比如foo。而你的程序里面既有main,又
有foo,则这种情况下,程序就先从foo开始执行。所以,main函数和其他
函数一样,并没有特殊地位。


    下面谈谈在uClinux中,main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单,基本上是平铺的,所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单,我还是用flat格式举例。这里暂不分析flat文件格式,我们把注
意力放到参数传递上。uClinux开发用户程序,首先当然是编码,然后编译,
编译生成的文件是elf格式的,所以要用工具elf2flt将elf文件转换成flat,
假设这个工作已经完成。


    我们在uclinux的shell下执行一个文件foo x y,foo是程序名,x, y是
参数。学过C语言的都知道,x,y作为参数会传递给main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候,操作系统会调用
do_execve(char *filename, char**argv, char**envp, struct pt_regs *regs),
这个操作会根据文件路径打开文件,装入内存,argv就是放到命令行参数,envp是
环境变量参数。


    在装入文件时,系统会根据不同的文件格式调用不同文件装入的handler,如果
是flat格式,就会调用load_flat_binary(),在fs/binfmt_flat.c中。有关参数,会
根据一路传递下来的argv,envp首先处理一遍计算出参数的个数argc,envc。然后在函
数create_flat_tables里面建立好参数表。整个函数代码如下:
 static unsigned long create_flat_tables(unsigned long pp, struct linux_binprm *
bprm)
{
(1) unsigned long *argv,*envp;
(2) unsigned long * sp;
(3) char * p = (char*)pp;
(4) int argc = bprm->argc;
(5) int envc = bprm->envc;
(6) char dummy;


(7) sp = (unsigned long *)              ((-(unsigned long)sizeof(char *))&(unsigned long) p);


(8) sp -= envc+1;
(9) envp = sp;
(10)    sp -= argc+1;
(11)    argv = sp;


(12)    flat_stack_align(sp);
(13)    if (flat_argvp_envp_on_stack()) {
(14)        --sp; put_user((unsigned long) envp, sp);
(15)        --sp; put_user((unsigned long) argv, sp);
(16)    }


(17)    put_user(argc,--sp);
(18)    current->mm->arg_start = (unsigned long) p;
(19)    while (argc-->0) {
(20)        put_user((unsigned long) p, argv++);
(21)        do {
(22)            get_user(dummy, p); p++;
(23)        } while (dummy);
(24)    }
(25)    put_user((unsigned long) NULL, argv);
(26)    current->mm->arg_end = current->mm->env_start = (unsigned long) p;
(27)    while (envc-->0) {
(28)        put_user((unsigned long)p, envp); envp++;
(29)        do {
(30)            get_user(dummy, p); p++;
(31)        } while (dummy);
(32)    }
(33)    put_user((unsigned long) NULL, envp);
(34)    current->mm->env_end = (unsigned long) p;
(35)    return (unsigned long)sp;
}
    (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针,sp是你要执行程序的用户区
堆栈,就是foo程序执行时,用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位,这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的,多余一个放0,表示envp数组结束。然后sp在移动argc+1各单位,留出argc+1
单位空间,这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的,多
余一个也放0,表示argv数组结束。经过堆栈调整,argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp,则现在envp=init_sp-(envc+1),
argv=envp-(argc+1)。


    (12)无关紧要,略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.


    (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->edummy, p); p++;
(31)        } while (dummy);
(32)    }
(33)    put_user((unsigned long) NULL, envp);
(34)    current->mm->env_end = (unsigned long) p;
(35)    return (unsigned long)sp;
}
    (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针,sp是你要执行程序的用户区
堆栈,就是foo程序执行时,用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位,这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的,多余一个放0,表示envp数组结束。然后sp在移动argc+1各单位,留出argc+1
单位空间,这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的,多
余一个也放0,表示argv数组结束。经过堆栈调整,argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp,则现在envp=init_sp-(envc+1),
argv=envp-(argc+1)。


    (12)无关紧要,略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.


    (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->envp[envc-1](也是由p所指)写入envp所指的堆栈区域中.
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.


    下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:



           --------------------------------
0x1f0000   |         0000                 |
           --------------------------------
0x1efffc   | envp[0] = 0x1c0008           | ---->指向"path=/bin"
           --------------------------------
0x1efff8   |         0000                 |
           --------------------------------
0x1efff4   | argv[2] = 0x1c0006           | ----->指向"y"
           --------------------------------
0x1efff0   | argv[1] = 0x1c0004           | ----->指向"x"
           --------------------------------
0x1effec   | argv[0] = 0x1c0000           | ----->指向"foo"
           --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.


    下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:



           --------------------------------
0x1f0000   |         0000                 |
           --------------------------------
0x1efffc   | envp[0] = 0x1c0008           | ---->指向"path=/bin"
           --------------------------------
0x1efff8   |         0000                 |
           --------------------------------
0x1efff4   | argv[2] = 0x1c0006           | ----->指向"y"
           --------------------------------
0x1efff0   | argv[1] = 0x1c0004           | ----->指向"x"
           --------------------------------
0x1effec   | argv[0] = 0x1c0000           | ----->指向"foo"
           --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
到r2-r6里来传递。当然,如果超过5个,就要借助堆栈了。


    既然main带了参数,那么在调用main之前,要把argc放到r2里面,argv放到r3里
面,envp放到r4里面。刚才说了,sp是用户空间堆栈起始地址。所以在开始执行foo
代码时候,r0=sp,在上文例子里r0等于0x1effe0.则如下伪汇编代码可以让参数装入
正确寄存器。


    load   r2, (r0)       /* r2 = argc */
    load   r3, (r0, 4)    /* r3 = argv */
    load   r4, (r0, 8)    /* r4 = envp */
    call   main           /* 跳转到main函数 */


    call   _exit


    以上代码就是最简单的进入main函数前的预处理。当然,不同系统不同格式文件处
理方式是不同的,刚才的一些例子是我碰到的一些情景和解决方案。


    我这个程序例子还没有完全讲完,比如后面printf怎么处理等,不过手都酸了,就
先讲到main函数的参数传递吧。刚学c语言那阵觉得main挺神秘,做过系统就知道,其实
main跟别的函数没有任何区别:)


 


路过

鸡蛋

鲜花

握手

雷人

发表评论 评论 (1 个评论)

cyzgod 2007-11-13 07:59
深刻阿,大哥