印象中的main函数
学过C语言的人一定知道,程序都是从main()函数开始的,不管你的程序有多么复杂。如果在深入一些,你会发现main()函数并不是程序执行的第一个函数,编译器早在之前就为main()函数的执行做下了铺垫。当然,不同平台上实现也不一样。再深入一点,你会发现main()函数是带参数的。举例来说,ISO/ANSI C标准规定main函数的标准语法如下:
int main (int argc, char *argv[])
其中第一个参数为程序执行时参数的个数,第二个参数是参数列表,从形式上可以看出它其实是一个字符型指针数组。用图来表示,看起来应该像这样(假设程序的名字为show_args):

在古老而严肃的UNIX系统上,main函数的定义更加复杂,比上面还要多一个参数:
int main (int argc, char *argv[], char *envp[])
这多出来的一个参数就是程序执行的环境变量,比如$PATH,
$HOME什么的。这些都是*nix系统才有的。当然,这些参数也不是非得从第三个参数传入,GNU/Linux上做了另外一套接口,getenv()/setevn()。同时,POSIX.1标准并不允许三个参数,考虑到程序的可移植性,建议使用两个参数的形式。如果确实需要环境变量,也请使用GNU提供的getenv()/setenv()接口。
argv[0]的传统
问题就出来了。从上图我们可以看到argv[0]就是程序本身的名字,这样给我们一个直觉:argv[0]就等于程序的名字。但是,事实上这只是一个传统(convention),并不是规定。这个传统是由外部维护的,GNU/Linux上就是由bash来负责准备着两个参数。程序是死的,人是活的。通过一些手法,可以绕过bash,直接给argv[0]赋任何想要的值。而这些魔法,来自于一个叫execv的系统调用。让我们来看看man 2 execv是怎么说的:
int execv(const char *path, char *const argv[]);
execv是exec()函数家族中的一员,其中一共有6个类似的函数,功能基本相同,就是启动另外一个进程,它们之间只有细微的差别。其中,path为需要执行程序的路径,argv就是给执行程序的参数列表。请注意,这里的argv[]是全部的参数列表,execv并不会做任何修改。所以,argv[0]就可以事先进行修改。秘密就在这里。以程序为证(exec.c):
/*exec --- run a program with a different name and any arguments */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
/* main --- adjust argv and run named program */
int main(int argc, char ** argv)
{
char * path;
if(argc < 3){
fprintf(stderr, "usage : %s path arg0 [ arg ... ]\n", argv[0]);
exit(1);
}
path = argv[1];
execv(path, argv + 2); /* skip argv[0] and argv[1] */
fprintf(stderr, "%s: execv() failed: %s\n", argv[0], strerror(errno));
exit(1);
}
这个程序的功能非常简单,就是做了一个代理,将传入的参数进行第二次调用。最后fprintf()的输出表明是出现错误。有点迷惑吗?这里没有做任何错误判断,每次调用都会执行这一行并输出错误。诡异的事情就是execv()这个函数。如果执行成功,它不会返回,永远。对,这是打破常规的一个函数,一个永远不会返回的函数。别忘了,这是一个系统调用,最终的处理是内核完成的。
确切的说,内核是这样处理的:当内核检查所有参数合法,会把原进程的进程地址空间上下文全部清除(不用担心,这不是异常终结,内核会优雅地处理这些事情。还有,argv这个参数会被内核从原来程序的堆栈上取出,在另外的地方妥善保存。)然后,内核加载目标程序代码,初始化新进程的环境变量,然后调用该程序的main()函数。于是,新的进程启动了。这里,新进程需要的argc参数是内核帮忙计算好的,argv参数则是原封不动的传送进去。
需要注意的是,这不是一个简单的fork()启动一个新进程,其实进程并不新,它的文件描述符列表还是继承原程序的。更加重要的是,新进程的PID和原进程的一样!execv()只是做了进程角色切换。打一个通俗的比方,一个人在一天之中有不同的角色,父亲,丈夫,朋友,雇员等。每天不同的时间,你的角色是不一样的。
好了,编译一下,看看效果:
$ ./exec /bin/grep whoami foo //执行 a line //这是用户的输入 a line with foo in it //继续输入 a line with foo in it //这是grep的输出
这里whoami被当成了argv[0]传给了grep,而grep忽略了,它需要的是argv[1]及以后的参数。所以这样执行与grep foo的效果是一样的。下一个例子:
$ ./exec nonexistent-program foo bar exec: execv() failed: No such file or directory
这里演示的是execv()执行失败,则返回,执行原进程的后续代码。还有一个有趣的实验:
$ ./exec ./exec foo usage: foo path arg0 [ arg ... ]
这又是为什么呢?屏幕前聪明的你仔细想一下,就会发现,这是自身调用,第一次调用成功,执行./exec foo,但后第二次调用的时候发现参数不足3个,遂退出。仔细想一下,这个和函数的递归调用非常类似,但事实上这里并没有任何堆栈的变化,只算得上一个特殊的进程调用。
自动化测试
有了make,最大的好处就是一切自动化。编写以下程序(show_args.c):
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char **argv)
{
int curarg = 0;
while(argc --)
{
printf("argv[%d] is %s\n", curarg++, *argv++);
}
exit(EXIT_SUCCESS);
}
两个文件放到一个目录,并编写Makefile进行自动化编译、测试:
default: exec show_args
exec: exec.o
cc -o exec exec.o
exec.o: exec.c
cc -c -Wall exec.c
show_args: show_args.o
cc -o show_args show_args.o
show_args.o: show_args.c
cc -c -Wall show_args.c
test: default
./exec ./show_args how are you
clean:
rm -f exec.o exec show_args.o show_args
运行make test,会得到以下输出:
... //编译过程略去 argv[0] is how argv[1] is are argv[2] is you
这个例子足以说明一切了。
结论
不要违背标准,更不要被标准禁锢思想。这个世界上没有不可能是事情,而创意就在于程序之中。

代码排版真是漂亮!
是么? 用SyntaxHighlighter, 直接用JavaScript渲染的,需要的话可以试一下