第三个例子这个例子代码同上一个一致,只不过我们将foo.c做成一个静态链接库libfoo.a进行链接,这里只给出Makefile的改动。 test: main.o foo.o ar rcs libfoo.a foo.o gcc -static -o test main.o libfoo.a main.o: main.c foo.o: foo.c clean: rm -f *.o test 运行情况如下: foo: (&b)=0x080ca008 sizeof(b)=8 b.a=2 b.b=4 main:0x08048250 parent: (&b)=0x080ca008 (&c)=0x080cc084 sizeof(b)=4 b=2 c=0 wait child... child: sleep(1) (&b):0x080ca008 (&c)=0x080cc084 sizeof(b)=4 set b=1 c=0 foo: (&b)=0x080ca008 sizeof(b)=8 b.a=1 b.b=4 main:0x08048250 parent: child over (&b)=0x080ca008 (&c)=0x080cc084 sizeof(b)=4 b=2 c=0 从这个例子看不出有啥差别,只不过使用静态链接后,全局变量加载的地址有所改变,b和c的地址之间似乎相隔更远了些。不过这次编译器倒是给出了变量b的sizeof决议警告。 到此为止,有些人可能会对上面的例子嗤之以鼻,觉得这不过是列举了C语言的某些特性而已,算不上黑。有些人认为既然如此,对于一切全局变量要么用static限死,要么定义同时初始化,杜绝弱符号,以便在编译时报错检测出来。只要小心地使用,C语言还是很完美的嘛~对于抱这样想法的人,我只想说,请你在夜深人静的时候竖起耳朵仔细聆听,你很可能听到Dennis Richie在九泉之下邪恶的笑声——不,与其说是嘲笑,不如说是诅咒…… 第四个例子/* foo.c */ #include <stdio.h> const struct { int a; int b; } b = { 3, 3 }; int main(); void foo() { b.a = 4; b.b = 4; printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n", &b, sizeof b, b.a, b.b, main); } /* t1.c */ #include <stdio.h> int b = 1; int c = 1; int main() { int count = 5; while (count-- > 0) { t2(); foo(); printf("t1:\t(&b)=0x%08x\n\t(&c)=0x%08x\n \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n", &b, &c, sizeof b, b, c); sleep(1); } return 0; } /* t2.c */ #include <stdio.h> int b; int c; int t2() { printf("t2:\t(&b)=0x%08x\n\t(&c)=0x%08x\n \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n", &b, &c, sizeof b, b, c); return 0; } Makefile脚本: export LD_LIBRARY_PATH:=. all: test ./test test: t1.o t2.o gcc -shared -fPIC -o libfoo.so foo.c gcc -o test t1.o t2.o -L. -lfoo t1.o: t1.c t2.o: t2.c .PHONY:clean clean: rm -f *.o *.so test* 执行结果: ./test t2: (&b)=0x0804a01c (&c)=0x0804a020 sizeof(b)=4 b=1 c=1 foo: (&b)=0x0804a01c sizeof(b)=8 b.a=4 b.b=4 main:0x08048564 t1: (&b)=0x0804a01c (&c)=0x0804a020 sizeof(b)=4 b=4 c=4 t2: (&b)=0x0804a01c (&c)=0x0804a020 sizeof(b)=4 b=4 c=4 foo: (&b)=0x0804a01c sizeof(b)=8 b.a=4 b.b=4 main:0x08048564 t1: (&b)=0x0804a01c (&c)=0x0804a020 sizeof(b)=4 b=4 c=4 ... 其实前面几个例子只是开胃小菜而已,真正的大坑终于出现了!而且这次编译器既没报错也没警告,但我们确实眼睁睁地看到作为main()中强符号的b被改写了,而且一旁的c也“躺枪”了。眼尖的读者发现,这次foo.c是作为动态链接库运行时加载的,当t1第一次调用t2时,libfoo.so还未加载,一旦调用了foo函数,b立马中弹,而且c的地址居然还相邻着b,这使得c一同中弹了。不过笔者有些无法解释这种行为的原因,有种说法是强符号的全局变量在数据段中是连续分布的(相应地弱符号暂存在.bss段或者符号表里),或许可以上报GNU的编译器开发小组。 另外笔者尝试过将t1.c中的b和c定义前面加上const限定词,编译器仍然默认通过,但程序在main()中第一次调用foo()时触发了Segment fault异常导致奔溃,在foo.c里使用指针改写它也一样。推断这是GCC对const常量所在地址启用了类似操作系统写保护机制,但我无法确定早期版本的GCC是否会让这个const常量被改写而程序不会奔溃。 至于volatile关键词之于全局变量,自测似乎没有影响。 怎么样?看了最后一个例子是否有点“不明觉厉”呢?C语言在你心目中是否还是当初那个“纯洁”、“干净”、“行为一致”的姑娘呢?也许趁着你不注意的时候她会偷偷给你戴顶绿帽,这一切都是通过全局变量,特别在动态链接的环境下,就算全部定义成强符号仍然无法为编译器所察觉。而一些IT界“恐怖分子”也经常将恶意代码包装成全局变量注入到root权限下存在漏洞的操作序列中,就像著名的栈溢出攻击那样。某一天当你傻傻地看着一个程序出现未定义的行为却无法定位原因的时候,请不要忘记Richie大爷那来自九泉之下最深沉的“问候”~ 或许有些人会偷换概念,把这一切归咎于编译器和链接器身上,认为这同语言无关,但我要提醒你,正是编译/链接器的行为支撑了整个语言的语法和语义。你可以反过来思考一下为何C的胞弟C++推出“命名空间(namespace)”的概念,或者你可以使用其它高级语言,对于重定义的全局变量是否能通过编译这一关。 所以请时刻谨记,C是一门很恐怖的语言! P.S.题外话写在最后。我无意挑起语言之争,只是就事论事地去“黑(hack)”一门语言而已,而且要黑就要黑得有理有力有层次,还要带点娱乐精神。其实黑一门语言并非什么尖端复杂的技术,个人觉得起码要做到两点:
|