Linux内核源码阅读系列(7)-链接4

可执行目标文件

静态连接的目标输出是“可执行的目标文件”,它的基本形式当然也是ELF。只是,“可执行目标文件”与“可重定位目标文件”一些重要的区别。产生这种区别的原因在于这两种文件的目的不同。“可重定位目标文件”的目的是提供给链接器(静态链接的ld或者动态链接的ld.so)链接信息以便帮助可执行文件。而“可执行目标文件”的目的在于提供一种形式将程序的代码方便的加载到内存中以便执行。所以,“可执行的目标文件”中增加了一个叫做“段头表”(segment header table)的部分,这个表中描述了文件内容到主存的映射方法,在程序运行时,加载器(loader)将根据这个表的信息把“可执行目标文件”中的代码拷贝到主存中去。

内存布局

下面的图展示了一个典型的可执行目标文件的构成,以及在取其相应的内存“镜像”之间的简单关系(并不严格)。

 +----------------------+
 |    ELF header        |
 +----------------------+     +--------------------+ 0x00000000
 | segment header table |     |       NO USAGE     |
 +----------------------+     +--------------------+ 0x08048000
 |       .init          | - > |  read-only segment |
 +----------------------+     |                    |
 |       .text          | - > |  (size <= 4kb*n)   |
 +----------------------+     |                    |
 |       .rodata        | - > |  ......            |
 +----------------------+     +--------------------+
 |       .data          | - > | read-write segment |
 +----------------------+     |  (size <= 4kb*m)   |
 |        .bss          | - > |  .....             |
 x----------------------x     +--------------------+
 /        .symtab       /     |       heap         |
 x----------------------x     +--------------------+
 /      .rel.text       /     |        ↓           |
 x----------------------x     |                    |
 /      .rel.data       /     +--------------------+ 0x40000000
 x----------------------x     |   shared library   |
 /       .debug         /     | memory map area    |
 x----------------------x     +--------------------+
 /       .line          /     |        ↓           |
 x----------------------x     |        ↑           |
 /       .strtab        /     +--------------------+
 x----------------------x     |      stack         |
 / section header table /     +--------------------+ 0xbfffffff
 x----------------------x     |      kernel        |
                              +--------------------+

在IA32结构上,Linux采用虚拟内存(virtual memory)技术,所以,每个程序在内存布局的时候都好像已经拿到了所有的内存一样,而程序代码最开始的地方总是在虚拟地址0x08048000处。加载器从这里开始拷贝ELF中定义的只读代码,这些只读代码通常被包含在.init.text.rodate段;所谓段和节事实上同样的,只是在链接的时候,它被称为“节”,而加载时却被成为“段”。.init段是链接器给每个“可执行目标文件”添加的,在其中包含了程序的初始化代码的一部分;链接器在其中写入了一个叫做_init的函数,这个细节需要注意。只读代码要求4kb对齐,所以虽然它实际的大小为往往小于4kb*n,但是其后紧跟的读/写段却需要从4kb*n的虚拟内存处开始。同样,读/写段也需要4kb对齐(想想why?)。读写段之后紧跟的是堆(heap)的内存区域,众所周知,这个区域是为了malloc函数群动态分配的内存准备的;此外,这个区域将根据需要向上(向高地址区域)增长。ELF文件中的.symtab.debug等内容并不会被加载进入内存,上图中用斜线表示了。操作系统还会为这个程序在内存的0x40000000处准备了动态加载的共享库代码区域;0xbfffffff处准备了另一个重要的运行时数据结构“栈”,这个区域主要用于程序中的过程调用,并且区域大小向下增长。用于应用程序的空间到这里结束了,紧跟在运行时栈的栈底后面的区域,也就是从0xc0000000开始就是内核代码了。

加载器(loader)通常是shell呼出的,但是任何应用程序都可以通过系统调用execve()调用加载器。

内存访问越界的时候通常你会被警告很奇怪的消息”segment fault”,并且程序终了。相信这个会帮助你,让你对“段”的记忆更加深刻些了,哈哈哈。这是一句很笼统的提示,但他说的就是你的指针在乱跳,可能对只读内存区域进行了写操作。

启动代码

加载器运行时,首先构造一个上面提到的那样的内存映像,然后根据“段头表”的指引,将程序代码拷贝到内存中。接着,他会跳转到程序的入口开始执行程序。提到c语言的程序入口,那可不就是大名鼎鼎的main函数么?这个说法没错,但是也不全对。真正的程序入口是一个叫做_start的函数,这个函数被包含在crtl.o文件中。这个目标文件是C语言运行时环境的一部分。它的大致的示意代码如下所示:

0x080480c0 <_start>        /*  .text段的入口点                 */
  call _libc_init_first     /* 启动.text节的代码通常是初始化c的库  */
  call _init               /* 启动_init代码,也就是在_init段中   */
  call atexit              /* 注册一些在程序结束时需要作的动作     */
  call main                /* 应用程序的入口点                  */
  call _exit               /* 结束应用程序,将控制权返还给操作系统  */

很明显,c语言的main函数是约定好的,如果没有这个函数程序将不能被执行。关于ctrl.o这个事情,让我想起面试国内某家公司的时候,曾被面试官问到这个问题;他问c程序在调用main之前需要做哪些动作,当时刚刚毕业,我的回答是现编的……,当然是错的很离谱,恩,往事不堪回首。如果你经常看到编译时或者运行时提示找不到crtl.o文件,恭喜你,你可以记住它了。嘿嘿。

动态链接和PIC代码

顾名思义,动态链接就是将链接过程从编译时挪到了运行时。这个内容写起来会有如“懒婆娘的裹脚”,各位看官可以参照IBM developerWorks的文章。但是动态链接对于立志做个好程序员的有痔青年是非常重要的内容,所以,如果有时间还是要认真研究的。简单的过程应该是像这样的

生成.so


gcc -shared -fPIC -o libvector.so x.c y.c z.c

这个过程就是将源代码文件编译成为目标文件,然后在用PIC指定它进行特殊的链接重定位定位信息,其中比较重要的就是添加PLT(procedure linkage table)和GOT(global offset table)。PLT被添加到.text节,而GOT被添加到.data节。PIC代码有个缺陷,就是因为对GOT的存储器引用造成的,具有大量寄存器堆的机器上没有太大问题,但是,寄存器不足的机器上却会造成严重缺陷。比如,MIPS结构的GOT问题就由来已久。对于外部过程的调用,PIC代码中采用一种叫做“延迟绑定”(lazy binding)技术,这个也是需要好好学习一下的。

链接共享库


gcc -o prog main.c libvector.so

这个链接过程不像静态链接过程,链接器并不真正的拷贝共享库中的.text.data节到可执行文件之中。相反,链接器会拷贝一些重定位和符号表信息,以便运行时可以解析对共享库代码和数据的引用。如你说知道的,这个过程就是在那个著名的$LD_LIBRARY_PATH之中去寻找共享库文件。

动态加载动态链接共享库


gcc -rdynamic -o prog main.c -ldl

通过dlopen()等函数可以在程序中动态地加载和链接共享库,在编译该程序时,只要连接libdl就可以了。而运行时,这个动态的加载和链接过程需要在被称为动态链接器的ld.so帮助下完成。这种方法特别灵活,因此被在各种各样的系统中广泛得应用,比如Java中的JNI(Java Native Interface),通过它可以让Java程序调用本地的C或者C++函数库。

这篇文章的目的在于说明一个程序是怎样形成,怎样加载,最终怎样在Linux中执行的,目的已经达到了,我就不再罗嗦了。

Linux内核源码阅读系列(6)-链接3

上一篇举例的时候那个例子并不是很恰当,因为用局部变量的生存周期来解释的话,也是行的通的。那个例子只能说是又添加了一种新的解释而已。要找一个比较妖的还挺难,就直接抄书了:

/* foo5.c */
#include <stdio.h>
void f(void);

int x = 15213;
int y = 15212;

int main()
{
	f();
	printf("x = 0x%x y = 0x%x n",
		x, y);
	return 0;
}
/* bar5.c */
double x;

void f()
{
	x = -0.0;
	^^^---链接器在处理这个符号x的时候,选择了,foo5.c文件中定义的
	      “强符号”int型的x,也就是解释为foo5.c中的x的内存位置写入
	      在这里定义的double型的值。
}

在IA32/Linux机器上,double型是8个字节,而int型是4字节;因此,这里将用double型的”-0.0″覆盖foo5.c中的x和y的内存位置,于是理所当然的程序出了一个意想不到的意外,而且这类错误是不容易被发现。

静态链接库

静态链接库就是把一堆相关的.o文件使用ar工具打包。最著名的静态连接库恐怕就是libc.a了。这是C语言标准库的静态链接版本。程序跟静态连接库链接的时候一般采用如下形式的命令:

$ gcc -O2 -c main.c
$ gcc -static -o swap_sample main.o libswap.a

程序在跟静态库链接的时候,首先链接器会按照命令行输入的从前往后的方向对可重定位文件进行符号解析,找出在模块内部未定义的符号,并将在其后找到包含这个符号定义的那个模块的代码和数据拷贝进入将生成的可执行目标文件,并对其中的符号进行重定位,如果这些未定义的符号全部解决,则链接成功并输出可执行文件,否则链接器会报错。

重定位

重定位就是确定一个对象(包括代码和数据)在存储器中的位置的过程。关于每个需要重定位的符号,链接器有两个方面事情要做,1. 对模块中的符号的定义(definition)进行定位,这个工作主要是合并各个输入模块的代码和数据节,并给每个节和每个符号定义赋以新的存储器地址; 2. 将模块中的引用(reference)指向正确的符号定义位置,这个工作主要依靠“重定位表目”完成,也就是上一篇中提到的实例中的rel.textrel.data节的总览中提到的”R_386_PC32“和”R_386_32“等附带有重定位类型的表目。

重定位表目可以用下面的包括下面代码展示的内容:

typedef struct {
	int offset;	/* 需要被重定位的“引用”在所在节中的偏移量 */
	int symbol:24,	/* 这个引用应该指向的符号 */
	    type:8;	/* 重定位类型 */
}

最重要的两类重定位类型就是”R_386_PC32″和”R_386_32″。

R_386_PC32: 这个类型的重定位信息主要控制的是程序在执行是的跳转。重定位一个使用32位PC(program counter)相关的地址引用。当CPU执行使用PC相关寻址的指令时,它就将在代码中编码的32位值加上PC当前运行时的值,得到有效地址,而PC值通常默认是存储器中的下一条指令的地址。

R_386_32:重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32值作为有效地址。————《深入理解计算机系统》

重定位符号应用的算法伪代码如下:

foreach section s {
	foreach relocation entry r {
		refptr = s + r.offset; /* 指向需要被重定位的引用的指针 */

	/* relocate a PC-relative reference */
	if (r.type = R_386_PC32) {
		refaddr = ADDR(s) + r.offset; /* 引用的运行时地址 */
		*refptr = (unsigned) ((ADDR(r.symbol) + *refptr - refaddr);
	}

	/* relocate an absolute reference */
	if (r.type == R_386_32)
		*refptr = (unsigned) (ADDR(r.symbol) + *refptr);
	}
}

R_386_PC32

在没有跳转的情况下,众所周知程序是按照从上到下的顺序顺序执行的,而这个事实在机器语言级别的直接反应就是PC的值默认情况下都会指向(经过call指令的计算后)当前执行指令的邻近下一条指令的地址。IA32结构中,一条指令的大小是4字节,所以,call指令的默认参数总是”-4″(0xfffffc),以便操作数于PC值相加时,跳转到临近的下一条指令。也就是说上面伪代码中的refptr在PC相关的地址引用中,初始值是”-4″。那么,如果程序发生非顺序执行的跳转,其重点因素就是要给call等类似的指令一个正确的操作数。这个操作数与PC中的值进行计算之后可以跳转到相应的对象(代码)保证程序的正确执行。”R_386_PC32″这种类型的重定位过程就是给call或者类似指令计算一个正确的操作数的过程。上面展示的伪代码中的refptr就是这个操作数。因为在给符号定义(definition)定位的过程中,ADDR(r.symbol)是确定的,所以,refptr就是可以计算的。

R_386_32

这种情况就简单些,计算方法只是将可重定位引用所在节的首地址和其偏移量相加,这样就能确定符号在虚存中的位置。

未完待续。

——写完后偷偷修改的分割线——-
这两天些的东西非常tmd的艰深难懂,但是硬骨头还是要啃的。市面上有很多SourceReview的书,但是读完之后总是觉得只见树木不见森林。我想要一个从上到下看到通通投投的Linux内核“解析体验”,哈哈哈。理论是比较枯燥,细节是比较烦人,但是所有奇妙的计算效果就是用这些东西为基础的,没有办法。很多时候也许真的需要不求甚解,但是,我是个偏执狂,如果遇到自己感兴趣却没有弄通的东西总觉得如鲠在喉。最终结果是写这样的文章难为自己,看这个样的文章吓走朋友,哈哈哈

Linux内核源码阅读系列(5)-链接2

可重定位目标文件实例解析

上回书说到ELF的文件格式,这里看一个真实的例子:用readelf工具窥看一下上篇提到的main.c编译而成的main.o文件。

$ gcc -O2 -g -c main.c -o main.o
$ file swap.o
swap.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

file命令的结果显示,main.o文件是一个386结构上的“可重定位目标文件”,并且包含调试信息。这个我使用的编译命令是相应的。用readelf工具读出main.o的细节看一下,将是下面这个样子。重要的部分直接插入了注解。

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
                                                      ^^^---小端
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
                                     ^^^---ObjectFile的类型,上篇提到的3种之一
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          900 (bytes into file)
                                     ^^^---这里指明了“节头表”的位置
  Flags:                             0x0
  Size of this header:               52 (bytes)
                                     ^^^---ELF头的大小
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
                                     ^^^---这里指明了“节头表”的大小
  Number of section headers:         23
  Section header string table index: 20

上面这个就是ELF的头部信息,头部信息主要提供了ELF文件适用的体系结构以及文件各个部分的定位信息,比如“节头表”的位置/大小等。当然还包括“节头表”中记录的数量。

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000040 000021 00  AX  0   0 16
  [ 2] .rel.text         REL             00000000 000854 000008 08     21   1  4
  [ 3] .data             PROGBITS        00000000 000064 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00006c 000000 00  WA  0   0  4
       ^^^---未初始化的全局变量将存放在此,但仔细看他的"Size"就知道,这个节是空的;
             事实上它通常就只是一个占位符。.bss这个名字来自于IBM 704汇编语言中的
             Block Storage Start指令的首字母缩写,鉴于它只是占位符号,可以记作
             Best Save Space。(来自于《深入理解计算机系统》)恩,随你怎么叫它。

  [ 5] .debug_abbrev     PROGBITS        00000000 00006c 000060 00      0   0  1
  [ 6] .debug_info       PROGBITS        00000000 0000cc 00006a 00      0   0  1
  [ 7] .rel.debug_info   REL             00000000 00085c 000060 08     21   6  4
  [ 8] .debug_line       PROGBITS        00000000 000136 000037 00      0   0  1
  [ 9] .rel.debug_line   REL             00000000 0008bc 000008 08     21   8  4
  [10] .debug_frame      PROGBITS        00000000 000170 000050 00      0   0  4
  [11] .rel.debug_frame  REL             00000000 0008c4 000010 08     21  10  4
  [12] .debug_loc        PROGBITS        00000000 0001c0 000043 00      0   0  1
  [13] .debug_pubnames   PROGBITS        00000000 000203 000023 00      0   0  1
  [14] .rel.debug_pubnam REL             00000000 0008d4 000008 08     21  13  4
  [15] .debug_aranges    PROGBITS        00000000 000226 000020 00      0   0  1
  [16] .rel.debug_arange REL             00000000 0008dc 000010 08     21  15  4
  [17] .debug_str        PROGBITS        00000000 000246 00004c 01  MS  0   0  1
  [18] .comment          PROGBITS        00000000 000292 00002a 00      0   0  1
  [19] .note.GNU-stack   PROGBITS        00000000 0002bc 000000 00      0   0  1
       ^^^---上面是一堆调试用的二进制内容。忽略之没有什么大碍。

  [20] .shstrtab         STRTAB          00000000 0002bc 0000c5 00      0   0  1
  [21] .symtab           SYMTAB          00000000 00071c 000120 10     22  15  4
       ^^^---符号表。这位神仙是链接过程处理的重点。

  [22] .strtab           STRTAB          00000000 00083c 000016 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

There are no program headers in this file.
^^^----“可重定位目标文件”并不包含program header table,这个表用于将目标文件
       映射到虚拟存储器,后文详述。
Relocation section '.rel.text' at offset 0x854 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000012  00001002 R_386_PC32        00000000   swap
                   ^^^---符号的重定位类型,一共有11种,现在遇到的这个是最重要的两种之一的
                         “与PC相关的地址引用”

Relocation section '.rel.debug_info' at offset 0x85c contains 12 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000501 R_386_32          00000000   .debug_abbrev
                   ^^^---符号的重定位类型,最重要之二,“32位地址引用”

0000000c  00000c01 R_386_32          00000000   .debug_str
00000011  00000c01 R_386_32          00000000   .debug_str
00000015  00000c01 R_386_32          00000000   .debug_str
00000019  00000201 R_386_32          00000000   .text
0000001d  00000201 R_386_32          00000000   .text
00000021  00000701 R_386_32          00000000   .debug_line
00000027  00000c01 R_386_32          00000000   .debug_str
00000031  00000201 R_386_32          00000000   .text
00000035  00000201 R_386_32          00000000   .text
00000039  00000901 R_386_32          00000000   .debug_loc
00000065  00001101 R_386_32          00000000   buf

Relocation section '.rel.debug_line' at offset 0x8bc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000002a  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_frame' at offset 0x8c4 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000018  00000801 R_386_32          00000000   .debug_frame
0000001c  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_pubnames' at offset 0x8d4 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info

Relocation section '.rel.debug_aranges' at offset 0x8dc contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info
00000010  00000201 R_386_32          00000000   .text

There are no unwind sections in this file.

这一段描述了需要重定位的符号或者代码总览。其中每一个记录都描述了需要重定位的对象存在于哪一节,以及它相对于节开始位置的偏移量。

Symbol table '.symtab' contains 18 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     0 SECTION LOCAL  DEFAULT    6
     7: 00000000     0 SECTION LOCAL  DEFAULT    8
     8: 00000000     0 SECTION LOCAL  DEFAULT   10
     9: 00000000     0 SECTION LOCAL  DEFAULT   12
    10: 00000000     0 SECTION LOCAL  DEFAULT   13
    11: 00000000     0 SECTION LOCAL  DEFAULT   15
    12: 00000000     0 SECTION LOCAL  DEFAULT   17
    13: 00000000     0 SECTION LOCAL  DEFAULT   19
    14: 00000000     0 SECTION LOCAL  DEFAULT   18
    ^^^---符号表的前14项无须特别关心,因为他们都是编译器自己加的默认值或者调试信息。

    15: 00000000    33 FUNC    GLOBAL DEFAULT    1 main
    16: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
    17: 00000000     8 OBJECT  GLOBAL DEFAULT    3 buf
    ^^^---在main.o文件中真正存在的符号。其中,第一列描述了符号的地址相对于“节”开始的位置的偏移量。
          第二列说明的是这个对象的大小,第三列是对象类型,Ndx指明了对象存在在哪个节中,其中"1"是
          .text节,"3"是.data节。如果是"UND",它指的是没有在这个ObjectFile中定义的符号,是需
          要重定位的对象。

No version information found in this file.

谢谢各位看官的耐心,这玩意看一遍赶紧忘了吧,如果记住就是把大脑当硬盘用了。之所以细节到这个地步,只为了说明一个问题“万事万物都是有原因的”,只要仔细一点都能弄清楚事情到底怎么了,而这种仔细和耐心正是现在国内很多程序员缺少的。我参加过的与中国工程师打交道的软件项目大都让我感慨万千,中国人做事的态度和日本人做事的态度简直没有办法相提并论,不知道什么原因让很多程序员都浮躁的烧到不行……最近参加了一些中文的邮件列表,发现这个风气尤为盛行,难道真的是民族性格问题?话又说回来,同样的工作,日本工程师拿着不止2倍于中国工程师的工资,好歹做事情的态度也应该对得起这份钱。

符号和符号表

上面提到的“符号”,就是“链接”阶段需要解决的重点问题的作业对象。链接需要解决被链接在一起的各个模块之间的符号和代码之间的联系,并重定位这些信息,以便生成的机器代码能够正确的跳转,或者正确的引用到某个变量的值。从链接器的角度看,符号可以分为三类 1.在本模块中定义,被其他模块引用的符号,这可能包括非静态(static)函数和非静态变量; 2.在其他模块中定义,本模块引用的符号,这种符号被称为外部符号(external symbol); 3.在本模块中定义并只在本模块中引用的符号,这种符号成为本地符号(local symbol),可能主要包含static函数和static全局变量。值得一提的是,本地符号并不等于程序的本地变量,因为众所周知,本地程序变量是在运行时栈中管理的,他们的生存周期很短,通过pop和push就能瞬间产生和消灭,无需符号表管理。

链接器的符号解析规则

本地符号的定义在链接过程中不会有大问题,每个本地符号都有唯一的定义;包括Java或者C++中的重载函数等,编译器会运用规则位有同名不同参数的函数各自产生一个唯一的“内藏”函数名。

全局符号比较麻烦,首先是要按照强弱分类,1.函数,已经初始化的全局变量是“强符号”;2.未初始化的全局变量是“弱符号”。然后是取舍规则,1.同名两强必出错,链接器报错; 2.同名强弱,肯定是选择强者; 3.同名两弱就随便取一个。注意啦,如果这个时候你还没有意识到明明规则存在的重要性的话,真是后知后觉了。此外,还有一个问题,就是为什么链接大批动态链接库时会有莫名其妙的错误?原因就是这些“潜规则”导致的错误了。比如,下面的例子:

文中有注释,c&p注意。

/* foo2.c */

void bar(void);

int x = 12345;
        ^^^---已经初始化的全局变量,“强”符号

int main()
{
	bar();
	printf("x = %dn", x);
	return 0;
}
/* bar2.c */
int x;
    ^^^---未初始化的全局变量,“弱”符号

void bar()
{
	x = 54321;
}

这个程序被链接的并运行的话,其结果让程序员大跌眼睛的,x的输出居然还是12345。而实际现实中的问题比这个不知道要复杂多少倍,往往非常难于发现和排除,所以好的命名习惯真的是在体现一个程序员和一个软件开发团队的素养,而不是为了符合CodeStyle做的面子工程。如果你使用gcc作编译器的话,开启-warn-common选项,将帮助你查找重定义的错误。微软的编译器肯定也有这个选项,但是我有n多年都没给Windows写过程序了,实在是不知道。

未完待续。
—-
文中例子来自《深入理解计算机系统》