再谈C语言位域

No Comments

我在日常工作中使用C语言中的位域(bit field)的场景甚少,原因大致有二:

* 一直从事于服务器后端应用的开发,现在的服务器的内存容量已经达到了数十G的水平,我们一般不需要为节省几个字节而使用内存布局更加紧凑的位域。
* 结构体中位域的实现是平台相关或Compiler相关的,移植性较差,我们不会贸然地给自己造“坑”的。

不过近期Linux技术内核社区(www.linux-kernel.cn) mail list中的一个问题让我觉得自己对bit field的理解还欠火候,于是乎我又花了些时间就着那个问题重新温习一遍bit field。

零、对bit field的通常认知

在C语言中,我们可以得到某个字节的内存地址,我们具备了操作任意内存字节的能力;在那个内存空间稀缺的年代,仅仅控制到字节级别还不足以满足C 程序员的胃口,为此C语言中又出现了bit级别内存的“有限操作能力” – 位域。这里所谓的“有限”指的是机器的最小粒度寻址单位是字节,我们无法像获得某个字节地址那样得到某个bit的地址,因此我们仅能通过字节的运算来设置 和获取某些bit的值。在C语言中,尝试获得一个bit field的地址是非法操作

struct flag_t {
    int a : 1;
};

struct flag_t flg;
printf("%p\n", &flg.a);

error: cannot take address of bit-field ‘a’

以下是C语言中bit field的一般形式:

struct foo_t {
    unsigned int b1 : n1,
                 b2 : n2,
                 … …
                 bn : nk;
};

其中n1,n2,nk为对应位域所占据的bit数。

位域(bit field)的出现让我们可以用变量名代表某些bit,并通过变量名直接获得和设置一些内存中bit的值,而不是通 过晦涩难以理解的位操作来进行,例如:

struct foo_t {
    unsigned int a : 3,
                 b : 2,
                 c : 4;
};

struct foo_t f;
f.a = 3;
f.b = 1;
f.c = 12;

另外使用位域我们可以在展现和存储相同信息的同时,自定义更加紧凑的内存布局,节约内存的使用量。这使得bit field在嵌入式领域,在驱动程序领域得到广泛的应用,比如可以仅用两个字节就可以将tcpheader从dataoffset到fin的信息全部表示 和存储起来:

struct tcphdr {
    … …
    __u16   doff:4,
            res1:4,
            cwr:1,
            ece:1,
            urg:1,
            ack:1,
            psh:1,
            rst:1,
            syn:1,
            fin:1;
    … …
};

一、存储单元(storage unit)

C标准允许unsigned int/signed int/int类型的位域声明,C99中加入了_Bool类型的位域。但像Gcc这样的编译器自行加入了一些扩展,比如支持short、char等整型类 型的位域字段,使用其他类型声明位域将得到错误的结果,比如:

struct flag_t {
    char* a : 1;
};
 error: bit-field ‘a’ has invalid type

C编译器究竟是如何为bit field分配存储空间的呢?我们以Gcc编译器(Ubuntu 12.04.2 x86_64 Gcc 4.7.2 )为例一起来探究一下。

我们先来看几个基本的bit field类型的例子:

struct bool_flag_t {
    _Bool a : 1,
          b : 1;
};

struct char_flag_t {
    unsigned char a : 2,
                  b : 3;
};

struct short_flag_t {
    unsigned short a : 2,
                   b : 3;
};

struct int_flag_t {
    int a : 2,
        b : 3;
};

int
main()
{
    printf("%ld\n", sizeof(struct bool_flag_t));
    printf("%ld\n", sizeof(struct char_flag_t));
    printf("%ld\n", sizeof(struct short_flag_t));
    printf("%ld\n", sizeof(struct int_flag_t));

    return 0;
}

编译执行后的输出结果为:
1
1
2
4

可以看出Gcc为不同类型的bit field分配了不同大小的基本内存空间。_Bool和char类型的基本存储空间为1个字节;short类型的基本存储空间为2个字节,int型的为4 个字节。这些空间的分配是基于结构体内部的bit field的size没有超出基本空间的界限为前提的。以short_flag_t为例:

struct short_flag_t {
    unsigned short a : 2,
                   b : 3;
};

a、b两个bit field总共才使用了5个bit的空间,所以Compiler只为short_flag_t分配一个基本存储空间就可以存储下这两个bit field。如果bit field的size变大,size总和超出基本存储空间的size时,编译器会如何做呢?我们还是看例子:

struct short_flag_t {
    unsigned short a : 7,
                   b : 10;
};

将short_flag_t中的两个bit字段的size增大后,我们得到的sizeof(struct short_flag_t)变成了4,显然Compiler发现一个基础存储空间已经无法存储下这两个bit field了,就又为short_flag_t多分配了一个基本存储空间。这里我们所说的基本存储空间就称为“存储单元(storage unit)”它是Compiler在给bit field分配内存空间时的基本单位,并且这些分配给bit field的内存是以存储单元大小的整数倍递增的。但从上面来看,不同类型bit field的存储单元大小是不同的

sizeof(struct short_flag_t)变成了4,那a和b有便会有至少两种内存布局方式:
* a、b紧邻
* b在下一个可存储下它的存储单元中分配内存

具体采用哪种方式,是Compiler相关的,这会影响到bit field的可移植性。我们来测试一下Gcc到底采用哪种方式:

void
dump_native_bits_storage_layout(unsigned char *p, int bytes_num)
{

    union flag_t {
        unsigned char c;
        struct base_flag_t {
            unsigned int p7:1,
                         p6:1,
                         p5:1,
                         p4:1,
                         p3:1,
                         p2:1,
                         p1:1,
                         p0:1;
        } base;
    } f;

    for (int i = 0; i < bytes_num; i++) {
        f.c = *(p + i);
        printf("%d%d%d%d %d%d%d%d ",
                         f.base.p7,
                         f.base.p6, 
                         f.base.p5, 
                         f.base.p4, 
                         f.base.p3,
                         f.base.p2, 
                         f.base.p1, 
                         f.base.p0);
    }
    printf("\n");
}

struct short_flag_t {
    unsigned short a : 7,
                   b : 10;
};

 struct short_flag_t s;
 memset(&s, 0, sizeof(s));
 s.a = 113; /* 0111 0001 */
 s.b = 997; /* 0011 1110 0101 */

 dump_native_bits_storage_layout((unsigned char*)&s, sizeof(s));
 
编译执行后的输出结果为: 1000 1110 0000 0000 1010 0111 1100 0000。可以看出Gcc采用了第二种方式,即在为a分配内存后,发现该存储单元剩余的空间(9 bits)已经无法存储下字段b了,于是乎Gcc又分配了一个存储单元(2个字节)用来为b分配空间,而a与b之间也因此存在了空隙。

我们还可以通过匿名0长度位域字段的语法强制位域在下一个存储单元开始分配,例如:

struct short_flag_t {
    unsigned short a : 2,
                   b : 3;
};

这个结构体本来是完全可以在一个存储单元(2字节)内为a、b两个位域分配空间的。如果我们非要让b放在与a不同的存储单元中,我们可以通过加入 匿名0长度位域的方法来实现:

struct short_flag_t {
    unsigned short a : 2;
    unsigned short   : 0;
    unsigned short b : 3;
};

这样声明后,sizeof(struct short_flag_t)变成了4。

 struct short_flag_t s;
 memset(&s, 0, sizeof(s));
 s.a = 2; /* 10 */
 s.b = 4; /* 100 */

 dump_native_bits_storage_layout((unsigned char*)&s, sizeof(s));

执行后,输出的结果为:

0100 0000 0000 0000 0010 0000 0000 0000

可以看到位域b被强制放到了第二个存储单元中。如果没有那个匿名0长度的位域,那结果应该是这样的:

0100 1000 0000 0000

最后位域的长度是不允许超出其类型的最大长度的,比如:

struct short_flag_t {
    short a : 17;
};

error: width of ‘a’ exceeds its type

二、位域的位序

再回顾一下上一节的最后那个例子(不使用匿名0长度位域时):

 struct short_flag_t s;
 memset(&s, 0, sizeof(s));
 s.a = 2; /* 10 */
 s.b = 4; /* 100 */

dump bits的结果为0100 1000 0000 0000

怎么感觉输出的结果与s.a和s.b的值对不上啊!根据a和b的值,dump bits的输出似乎应该为1010 0000 0000 0000。对比这两个dump结果不同的部分:1010 0000 vs. 0100 1000,a和b的bit顺序恰好相反。之前一直与字节序做斗争,难不成bit也有序之分?事实就是这样的。bit也有order的概念,称为位序。位域字 段的内存位排序就称为该位域的位序。

我们来回顾一下字节序的概念,字节序分大端(big-endian,典型体系Sun Sparc)和小端(little-endian,典型体系Intel x86):
大端指的是数值(比如0×12345678)的逻辑最高位(0×12)放在起始地址(低地址)上,简称高位低址,就是高位放在起始地址
小端指的是数值(比如0×12345678)的逻辑最低位(0×78)放在起始地址(低地址)上,简称低位低址,就是低位放在起始地址

看下面例子:

int
main()
{
    char c[4];
    unsigned int i = 0×12345678;
    memcpy(c, &i, sizeof(i));

    printf("%p – 0x%x\n", &c[0], c[0]);
    printf("%p – 0x%x\n", &c[1], c[1]);
    printf("%p – 0x%x\n", &c[2], c[2]);
    printf("%p – 0x%x\n", &c[3], c[3]);
}

在x86 (小端机器)上输出结果如下:

0x7fff1a6747c0 – 0×78
0x7fff1a6747c1 – 0×56
0x7fff1a6747c2 – 0×34
0x7fff1a6747c3 – 0×12

在sparc(大端机器)上输出结果如下:

ffbffbd0 – 0×12
ffbffbd1 – 0×34
ffbffbd2 – 0×56
ffbffbd3 – 0×78

通过以上输出结果可以看出,小端机器的数值低位0×78放在了低地址0x7fff1a6747c0上;而大端机器则是将数值高位0×12放在了低 地址0xffbffbd0上。

机器的最小寻址单位是字节,bit无法寻址,也就没有高低地址和起始地址的概念,我们需要定义一下bit的“地址”。以一个字节为例,我们把从左到右的8个bit的位置(position)命名按顺序命名如下:

p7 p6 p5 p4 p3 p2 p1 p0

其中最左端的p7为起始地址。这样以一字节大小的数值10110101(b)为例,其在不同平台下的内存位序如下:

大端的含义是数值的最高位1(最左边的1)放在了起始位置p7上,即数值10110101的大端内存布局为10110101。
小端的含义是数值的最低位1(最右边的1)放在了起始位置p7上,即数值10110101的小端内存布局为10101101。

前面的函数dump_native_bits_storage_layout也是符合这一定义的,即最左为起始位置。

同理,对于一个bit个数为3且存储的数值为110(b)的位域而言,将其3个bit的位置按顺序命名如下:

p2 p1 p0

其在大端机器上的bit内存布局,即位域位序为: 110;
其在小端机器上的bit内存布局,即位域位序为: 011

在此基础上,理解上面例子中的疑惑就很简单了。

 s.a = 2; /* 10(b) ,大端机器上位域位序为 10,小端为01 */
 s.b = 4; /* 100(b),大端机器上位域位序为100,小端为001 */

于是在x86(小端)上的dump bits结果为:0100 1000 0000 0000
而在sparc(大端)上的dump bits结果为:1010 0000 0000 0000

同时我们可以看出这里是根据位域进行单独赋值的,这样位域的位序是也是以位域为单位排列的,即每个位域内部独立排序, 而不是按照存储单元(这里的存储单元是16bit)或按字节内bit序排列的。

三、tcphdr定义分析

前面提到过在linux-kernel.cn mail list中的那个问题大致如下:

tcphdr定义中的大端代码:

__u16   doff:4,
        res1:4,
        cwr:1,
        ece:1,
        urg:1,
        ack:1,
        psh:1,
        rst:1,
        syn:1,
        fin:1;

问题是其对应的小端代码该如何做字段排序?似乎有两种方案摆在面前:

方案1:
__u16    res1:4,
         doff:4,
         fin:1,
         syn:1,
         rst:1,
         psh:1,
         ack:1,
         urg:1,
         ece:1,
         cwr:1;

or

方案2:
__u16   cwr:1,
        ece:1,
        urg:1,
        ack:1,
        psh:1,
        rst:1,
        syn:1,
        fin:1,
        res1:4
        doff:4;

个人觉得这两种方案从理论上都是没错的,关键还是看tcphdr是如何进行pack的,是按__u16整体打包,还是按byte打包。原代码中使用的是方 案1,推测出tcphdr采用的是按byte打包的方式,这样我们只需调换byte内的bit顺序即可。res1和doff是一个字节内的两个位域,如果 按自己打包,他们两个的顺序对调即可在不同端的平台上得到相同的结果。用下面实例解释一下:

假设在大端系统上,doff和res1的值如下:

doff res1
1100 1010 大端

在大端系统上pack后,转化为网络序:

doff res1
1100 1010 网络序

小端系统接收后,转化为本地序:

0101 0011

很显然,我们应该按如下方法对应:

res1 doff
0101 0011

也就相当于将doff和res1的顺序对调,这样在小端上依旧可以得到相同的值。

果果3周岁了

1 Comment

果果已经3周岁了,这是一个不争的事实。这意味着我又变老了^_^。过去的东西已经无法抓住了,目前我能做的就是欣赏现实了^_^。

3岁的果果长的越来越有女孩儿的味道了^_^。

3岁的果果生长发育一切良好,个头还是比同龄的孩子高出那么一截。

3岁的果果说起话来越来越有逻辑性了,我们时常惊诧于其时而冒出的“妙语”。

3岁的果果总是说“喜欢爸爸”,因为妈妈总是加班,而无暇陪着果果玩。

3岁的果果很有自尊心,任何事情都不甘落后于其周围的小朋友。

3岁的果果更爱臭美了,喜欢让爸爸妈妈买新衣服新鞋子,买完后立马穿上向我们展示。

3岁的果果打针已经不掉一滴眼泪了,这让我们着实有些惊奇。

3岁的果果已经完全会自己穿衣脱衣了,只是费点力气罢了^_^,并且拒绝我们的帮助。

3岁的果果已经会唱好几首儿童歌曲了,只是还不在调子上。

3岁的果果发音还不准确,尤其是在说成语、背唐诗时,也就是我和她妈妈能听懂个一二吧。

3岁的果果对奶粉的依恋降低了,有时候还很不情愿的喝奶。

3岁的果果开变得更加喜欢吃肉了,啃起排骨就不放下了。

3岁的果果喜欢听爸爸给她讲故事,并且有时还可以简单复述出爸爸讲的简单故事了。

3岁的果果很有想象力,经常用积木拼出爸爸都看不出是啥的物品,而她却很肯定地说这是…。

3岁的果果尤其喜欢玩过家家游戏,更喜欢让爸爸当观众,看她是如何玩过家家的游戏的,在游戏中她既扮演老师,又扮演学生。

3岁的果果正健康茁壮的成长,这正是我这个做爸爸期望看到的。

以下是果果近期的一些生活照片:

我是小公主

呵呵,出去旅游了!

我秀气不?

霸气

我淑女不?

不好意思了

再来一张

我飞,我飞,我飞!

buildc 0.3.0版本发布

No Comments

buildc正式在项目中应用以来,我们收到了许多同事针对buildc演进的意见和建议。其中确实有些易用性的问题是在最初设计时未考虑周全的,尤其是.buildc.rc中的配置,同事们对该文件的配置已经“怨声载道”了。

.buildc.rc是用来配置某开发者在开发过程中使用的第三方库所在subversion repository信息的,例如:

a_repository = ('SVN库地址', '本地缓存路径',
              [
                  # 格式:[(“第三方库名称”, “库版本”, “特征库文件”), …]
                  ('libevent', '2.0.10', 'lib/libevent.a'),
                  ('instantclient', '10.2.0.5.0', 'lib/libnnz10.so'),
                  …
              ]
            )
b_repository = ('SVN库地址', '本地缓存路径', [])
c_repository = ('SVN库地址', '本地缓存路径', [])

external_repositories = [
                        a_repository,
                        b_repository,
                        c_repository,
                        …
                   ]

这里面需要维护最多、最频繁的就是各个repository中具备的第三方库名、版本号。开发者所开发的项目所依赖的第三方库信息发生变化,不仅仅需要修 改project下的buildc.cfg文件,还可能要修改.buildc.rc,大家维护起来确实体验不好,会多耗费一些工作量。

针对这个主要问题,我们决定对buildc进行一次较大范围的重构,重构后的版本定为buildc 0.3.0版本。以下是buildc 0.3.0版本的主要改动点:

一、简化.buildc.rc的配置,重新定义cache相关命令的语义

0.3.0及以后版本的.buildc.rc只需配置repository的地址信息以及cache缓存的本地路径信息,无需再提供repository 里面具体的第三方库以及版本号信息了,这样一来,大多数情况下,project依赖的第三方库发生变更,都无需修改.buildc.rc了。

a_repository = ('SVN库地址', '本地缓存路径')
b_repository = ('SVN库地址', '本地缓存路径')
c_repository = ('SVN库地址', '本地缓存路径')

external_repositories = [
                        a_repository,
                        b_repository,
                        c_repository,
                        …
                   ]

随之而变的是buildc cache相关命令的语义,0.3.0中cache相关命令的语义如下:

* buildc cache init - 生成.buildc.repository,该文件是svn库的目录结构文件,相当于一份svn repository内部的地图,repository中存放的各种第三方库以及版本均在该文件中索引;如果该文件已经存在,命令执行的结果为:提示已存在。

* buildc cache upgrade – 根据.buildc.rc的最新更新,重新生成.buildc.repository文件,并将该文件中所有lib本地的 Revision号置为none。该文件并不会执行本地cache的library的真实更新操作。

* buildc cache update  - 
    1. 如果.buildc.rc已经修改,但没有执行buildc cache upgrade,update会对比本地缓存库信息与对应的.repository文件中的同名lib信息,如果不一致,则提醒执行upgrade。
    2. 如果.buildc.repository是新生成的,所有lib本地的Revision号均是none,则提示没有要更新的本地缓存库;
    3. 如果某个项目已经download了自己依赖的库,那update将比对svn库中和本地库的revision差异,并下载最新库版本。并修改.buildc.repository中对应库的本地revision number。

* buildc cache remove – 将.buildc.repository中对应库的本地revision number都置为none,并删除本地缓存的库文件。

二、重新定义config make的语义

前面提到了,在执行buildc cache init时,buildc只是负责生成.repository文件,而并不真实执行库文件的下载和缓存。那何时真正下载呢?答案是在执行buildc config make时。这里颇有些“lazy evaluate”的味道,需要时再“download and cache it"。

* buildc config make

1. 如果.buildc.rc已经修改,但没有执行buildc cache upgrade,config make会对比本地缓存库信息与对应的.repository文件中的同名lib信息,如果不一致,则提醒执行upgrade。
2. 如果.buildc.rc是新生成的,或执行cache upgrade后的,config make会根据project对应的buildc.cfg中配置的第三方库,在.buildc.repository中查找是否存在(包括对应的版本 号),如果存在,则从subversion server端自动下载;否则提示出错。
3. 如果本地缓存中某个库文件不存在,buildc config make会检测到,并自动下载该库,并cache起来。
4. 如果subversion端某个库的svn revision号发生的更新,buildc config make会检测到,并下载最新的版本。

总之一切都是在buildc config make时来完成的,按需下载或更新,这样你甚至无需进行手工的library Cache维护。

三、转向OO范型

实现buildc 0.3.0的小同事(wtz1989227@gmail.com)对OO情有独钟,因此在这个版本中,他将以前的结构化代码做了大幅度调整,并用OO的方 式进行了重构。按照wtz的思路,这次改造比较初级,OOD做得还不够充分,以后慢慢调整。实际代码中反映出来的情况也的确是这样。

四、buildc 0.2.3发布

在将buildc 0.3.0代码merge到trunk之前,我创建了buildc-0.2的maintain branch,虽然理论上buildc 0.3.0在功能和配置方面与buildc 0.2.x版本是兼容的,但毕竟代码调整幅度较大。另外建议大家都转移到0.3.0这个最新版本上来,buildc-0.2分支顶多做一些bugfix, 不会再有新feature添加进去了。

昨天在发布buildc 0.3.0的同时,还发布了buildc-0.2的一个Bugfix版 – buildc 0.2.3,该版本主要做了如下一些fix:

  * 执行cache upgrade时增加对.buildc.rc中repository特征文件存在性的检查;
  * 执行config make时增加对Make.rules文件是否为空的判断;
  * 执行pack source时,添加VERSION文件,记录打包的上下文信息。

五、其它

考虑到github的活跃度远远高于google code,加上google code最近访问十分不稳定,因此之前就将buildc(还有cbehavelcut以及我的实验代码库)fork了一份到github上了,也攒攒 github上人气,因此这次buildc 0.2.3和buildc 0.3.0的代码还要发布到github上一次。git工具平时用的少,尤其是提交代码到github,这次算入门了。

* 代码远程提交
用git remote add一个github的remote repository后,就可以使用git push origin master将本地的commit推送到github上了。

* 打tag,并推送tag

   — 查看Tag的git命令是git tag
   — 本地打tag,用这个命令: git tag -a v0.2.3 -m"0.2.3 released"
   — 推送Tag到remote repository:git push –tags origin master,不加–tags是无法推送tag的。

* branch操作
  — 查看branch:git branch
  — 创建branch:git branch buildc-0.2
  — 推送branch:git push origin buildc-0.2
  — 本地切换branch:git checkout buildc-0.2

也谈Commit log

No Comments

版本控制工具大行其道的今天,作为程序员,势必要每天与各种版本控制系统(比如SubversionGitMercurial等)打交道, 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单,对于一个专业程序员 来说,我们还要关注每次commit所携带的背景信息,这里暂且称之为“commit context”。在每次commit时,这些上下文信息只能通过commit log来体现。

一、Commit Context

今日的软件复杂度日益增加,软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了,而在团队模式下,版本控制系统发挥着至关重要的作用, 它让开发过程变得有序,将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的,它只是机械地记录着每次提交前后的内容的raw差 异,至于这个差异究竟代表了什么,版本管理系统是不得而知的,这就需要我们开发者们来提供,这就算是产生commit context的动机吧。即便是一个人开发维护的项目,个人的记忆也是有时效性的,时间久了,以前的代码变更context势必也就淡忘了,良好且规范的 commit context有助于更好的维护项目,追踪历史思路和行为,甚至在查找bug时也是能帮得上大忙的,比如确认bug引入的时段边界、代码范围等。

前面说了,commit context最终是以commit log形式提供的,这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏,无论是商业项目,还是开源项目,代码本身质量是一个重要的方面,代码 维护的规范性则是另外不可忽略的一个重要因素,而在代码维护规范性方面,commit log的规范是一项重要内容。做了这么多年Coding工作,到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的,这也是促使我思考commit log这块内容的一个初衷。

commit log承载着每次commit动作的context。一般来说context中至少要有一项内容,那就是此次代码变更的summary,这是最基本的要 求。如果你的commit log还是空着的,那你真该反思反思了,那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目,commit context涉及到的因素往往不止一个,很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。

[example1 - Linux Kernel]

audit: catch possible NULL audit buffers
It's possible for audit_log_start() to return NULL.  Handle it in the
various callers.

Signed-off-by: Kees Cook <keescook@chromium.org>
Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Eric Paris <eparis@redhat.com>
Cc: Jeff Layton <jlayton@redhat.com>
Cc: "Eric W. Biederman" <ebiederm@xmission.com>
Cc: Julien Tinnes <jln@google.com>
Cc: Will Drewry <wad@google.com>
Cc: Steve Grubb <sgrubb@redhat.com>
Cc: Andrea Arcangeli <aarcange@redhat.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

这是Linux Kernel项目的一个commit log的内容。从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的内核模块范围audit
- 修改的原因summary: to catch possible NULL audit buffers
- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表
- 这个patch由Who sign-off的。

将mail list放入到commit log中,这是Linux Kernel开发过程规范所要求的,同样也是质量保证的一个方法。在《如何加入Linux内核开发社区》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。

[example2 - Apache Subversion]

Fix issue #3498 – Subversion password stores freeze Eclipse

* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c
  (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,
   ssl_client_cert_pw_gnome_keyring_first_creds,
   ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked
    and we are in interactive mode but have no unlock prompt function, don't
    throw a "GNOME Keyring is locked and we are non-interactive" error;
    instead, continue without unlocking it, so that the unlocking may be
    handled by the default GNOME Keyring unlock dialog box.

这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c,包括括号中的函数名列表, 这个显然更为细致。
- 修改的原因summary: Fix issue #3498 – Subversion password stores freeze Eclipse
- 这个patch与问题跟踪系统的关联性 -issue #3498

通过这个commit log,我们可以快速找到此patch对应的问题跟踪系统中的条目#3498,这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。

综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的“全面”是project-dependent的,是需要能够体现出涉及project的一切必要信息的:过程的、质量的、工具 的。

二、Commit log格式

Commit log没有放之四海而皆准的统一格式,而是project-dependent的。就我个人而言,我会在下面的几个问题上有纠结。

* 语言

不得不承认在创造编程语言方面,西方文化占了主导,语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式,但却是很少见到在源文件中使用非英语命名变量或函数的,这也影响了我在commit log中对语言的选择 – 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的,你用中文提交也是没有任何问题的,尤其是在国内商业项目中,使用中文描述起来,理解上快且歧义少。我是不反对用中文写commit log的,但反感的是中英文混合写commit log(有些人用中文,有些人用英文)。每当批量看commit log时,中英文混在一起,一点美感都没有了。

commit log不是给最终用户看的,而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利,精确全面地传达context。即便 应用是要发布给非洲人民,但若开发人员都是中国人,一样可以用中文编写commit log。

* 地道

说到“地道”,主要是针对你选择外语(大多数情况是英语)作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的,我们在用英语编写commit log时也要学会选用“地道”的词汇,远离Chinglish。当然想立即做到“地道”也不是那么容易,毕竟我们一直以来就按照Chinglish的思维去学 习英语的,一个比较好的方式就是多看看知名开源项目(比如linux kernel)的commit log,看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少,看多了也就找猫画虎的学会了。

* 规范

“没有规矩,不成方圆”,无论是商业软件项目,还是大型开源项目,莫不如此。如果要想很好的传达commit context,一个设计规范,内容全面的commit log格式是必不可少的。我们无需从头做起,很多开源项目在这方面都已经有一些良好的实践,比如上面提到的linux kernel的commit log convention,再比如这里有Apache Subversion的Commit log要求。TYPO3和FLOW3也有自己详细的Commit log说明

制定规范时总体来说,注意以下几点:
– 格式简明扼要,只保留必要的项;
– 注意与项目过程、质量保证流程的结合,以及与第三方工具的关联(注意序号或ID的唯一性);
– 对于规模较大的系统,可以考虑在log中体现影响的涉及的“子模块”或“子目录”名字或者逻辑功能的名字(比如前面linux kernel例子中的audit),这样便于快速定位本地commit的影响范畴。

三、Commit模板

如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成(比如问题跟踪系统、代码评审系统等)时,建议设置Commit log template(模板)以简化开发者commit log编写的工作。

* Subversion命令行客户端支持commit log模板

Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能:

- 创建commit log模板log.tmpl,放在特定目录下,本例中放在用户的$HOME目录下
- 添加并导出环境变量SVN_EDITOR
         export SVN_EDITOR="rm svn-commit.tmp && cp ~/log.tmpl svn-commit.tmp && vi "

svn commit时,svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令,而svn-commit.tmp文件已经被替换为我们的模板文件,开发者只需按模板填写内容,并保存退出即可。如果 commit成功,svn客户端会删除当前目录下的svn-commit.tmp,否则svn-commit.tmp不会被删除,这将导致下次再提交 时,svn客户端检测到svn-commit.tmp的存在,从而新建立一个svn-commit.2.tmp的新文件,导致模板失效,这也是这个方法的 一个瑕疵。

* Git命令行支持commit log模板

Git是目前very hot的分布式版本管理工具,起步晚,但起点高,因此已经内置了对模板的支持,只需将模板文件配置一下即可。
         git config –global commit.template ~/log.tmpl

四、良好格式commit log的实施

即便有了良好格式的commit log的模板定义,但就我经验而言,实施起来也还会遇到诸多问题。commit行为是客户端发起的,要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署pre-commit hook,对提交的log格式进行检查,不符合模板格式的予以拒绝等。

对于与问题跟踪系统有关联的log格式,还要注意保持问题跟踪系统id或序号的唯一性,这显然是管理和过程方面的工作。

对于开源项目,一般merge到trunk需要owner的检查,所以反倒实施起来容易了些,只要有一篇内容丰富的 developer/community guide或convention之类的文档即可,多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的,为这些project提交patch前是要好好阅读这些文档的,不能坏了规矩^_^。     
 

推动知识管理的这两年

No Comments

掐指算来,部门知识管理的推广工作已有两年了。两年时间不能算短,但对于知识管理这件事来说,只能算是热身阶段,我们依旧站在起跑线上,或者稍乐 观地讲我们只是刚刚迈出了万米长跑的第一步。

下面是这两年来部门内部知识库建设的一个Timeline:

- 2011年中旬,我所在产品线私下在一台PC上建立了基于MediaWiki的知识库。
- 2011年末产品线在部门内部做了有关知识库与知识管理实践的分享。
- 2012年初,部门在新采购的高性能服务器上建立基于MediaWiki的知识库,并指定专人负责;我们产品线将已经积累的内容迁移到了部门知识库中,这 也标志着部门知识库1.0版本正式上线。知识管理的策划和推广事宜也交由专门的子部门负责。
- 2012年中,设立子部门KM负责人,设立子部门KM定期工作会,设立子部门技术交流汇报会,旨在各子部门之间分享最新信息,减少重复劳动,提高效率。
- 2012年末,启动知识库2.0建设方案。
- 2013年3月末,知识库2.0版本上线。邀请专业设计人员策划和实现了全新主页,提高了UE;重新策划了分类;重新划分了知识版块,专人负责更新;增加 了知识达人等多个激励内部童鞋分享知识的手段和方法;通过piwik统计和分析知识库的最新访问动态;通过一些实用的插件来简化Wiki Page编写工作、更好地展示内容;提炼高质量知识文章,形成知识周刊、月刊,作为内部知识库营销推广手段,吸引大家来到知识库,并尝试留下自己的知识。

两年来,我这个“始作俑者”在知识库建立起来后已经不做什么具体的工作了(骨子里其实是不愿意做重复性、事务性工作),只是充当着“幕后推手”。值得我庆幸的是有那么几位同事都认同知识管理 的重要性,愿意参与进来执行具体的工作。专职负责知识管理的子部门的领导也十分重视此事,这才有了部门知识库的持续演进,才有了目前的2.0版本 上线,他们才是真正的猪角。

这两年来,我在知识管理方面所作的工作主要有如下几方面:

* 找人,形成圈子

知识管理和推广虽然重要,但并非核心业务,不能显式地让大家看到其对部门发展的贡献度,因此多数人对此工作并不感兴趣,找到适合且对此有兴趣和热 情的人也就并非易事。另外还要得到相关子部门领导的长期支持,事情才好持续办下去。在1.0上线后,经过大半年的观察,我们找到了真正合适的人 选。也有两位志同道合的子部门领导十分重视此事,也亲自参与到知识库建设的交流讨论中。这样一个知识管理和推广的小圈子形成了。

* 识别广泛的需求,形成可行性共识

最初之所以在产品线私下建立起Wiki知识库,显然是因为我们遇到了诸多具体问题,诸如知识如何共享、知识的发现、知识更新以及一致性等(那时的 知识局限在项目过程中的各种文档资料等)。我们想通过一个共享的协作平台解决掉遇到的问题,于是有了我们自己的Wiki。这些问题其实是有共性 的,我们遇到了,其他产品线、开发组、子部门也会遇到。也就是说这个Wiki不仅仅能解决我们的问题,还能帮助解决其他人的问题。为此,我们做了 多次公开调查和私下交流,确定了知识库的必要性和大力推广的可行性。

* 保持与知识库的直接负责领导常沟通

我顶多算是一个“推手”,具体的知识库运营是由某子部门领导负责的。因此在用人以及知识库演进方面,还要常与领导沟通,达成一致后,推动执行起来 就方便的多了。

* 元策划

所谓的元策划就是为负责策划的具体执行人提供策划咨询,指导如何策划,仅此而已。当然有时也提供具体策划思路^_^。

* 监督实施

这个很关键。虽然我不直接负责知识管理这块,但我心目中是有一些期望达成的里程碑点的。因此我会不时的与具体的执行人了解进度情况,也算是一种督促和监督了。

知识库2.0上线一月有余,他们弄了个知识月刊首期,居然把我评为月度“知识达人”,还问我是否可以分享些知识积累和总结方面的心得。以前从未系统考虑过这个问题,冷不丁的提起来还真没啥思路。不过花些时间深入想了想,还是有点体会的,也许这个体会比较另类。

我承认我日常喜欢做一些知识积累和总结,只是喜欢并习惯为之而已,谈不上什么擅长,无论是工作中还是业余时间的学习过程中。为什么会这样呢?这么做到底动 机何为?我也仔细想了一下:从心理上来说这可能是源于一种“忧患意识”吧。真的是忧患:担心记性差,导致设计思路等知识和技巧的遗忘,那可真是种浪费和损 失;担心无地儿去回顾/查找(因此要起个好标题,找个好分类,贴上适当标签);担心体验和心得的消失;担心自己每天没有进步(一直追求每天进步一点点,而 积累和总结则是一种显式地进步的体现);担心别人看不到自己最新更新的内容(因此放到Wiki这种载体);担心大脑容量不够,无法装得下那么多内容,所以 持久化到一类“永恒”的介质(blog、wiki)中;担心自己说不清楚,讲不明白,就写下来,并反复揣摩修改,直到自己满意;担心太多的东西放在大脑 中,太沉重,无法轻装前进,因此写出来,腾出一些空间,容纳点新东西等等。

两年了,还是那句话,自己在知识管理方面依旧是野路子+新手!估计自己以后依旧不会直接做知识管理方面的执行工作,但肯定会是一个知识分享者以及一个旁观 参与者。知识库的建立为组织内的每个人、项目、产品线、子部门提供了一个分享的平台,也是一个自我展示的平台。知识库的内部营销才刚刚上路,前途光明,道 路坎坷,猪脚们要有一颗耐心。

最后和大家分享一下我们知识库的slogan:“知识不怕从头积累,就怕从不积累”

libiconv库链接问题一则

No Comments

与在Solaris系统上不同,Linux的libc库中包含了libiconv库中函数的定义,因此在Linux上使用libiconv库相关函数,编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题,到底是怎样一回事呢?这里分享一下问题查找过程。

一、现场重现

这里借用一下这位同事的测试程序以及那台机器,重现一下问题过程:
/*test.c */


#include <iconv.h>

int main(void)
{
    int r;
    char *sin, *sout;
    size_t lenin, lenout;
    char *src = "你好!";
    char dst[256] = {0};
    iconv_t c_pt;  

    sin = src;
    lenin = strlen(src)+1;

    sout = dst;
    lenout = 256;

    if ((c_pt = iconv_open("UTF-8", "GB2312")) == (iconv_t)(-1)){
        printf("iconv_open error!. errno[%d].\n", errno);
        return -1;
    }

    if ((r = iconv(c_pt, (char **)&sin, &lenin, &sout, &lenout)) != 0){
        printf("iconv error!. errno[%d].\n", r);
        return -1;
    }  

    iconv_close(c_pt);

    printf("SRC[%s], DST[%s].\n", src, dst);

    return 0;
}

根据之前的经验,我们按如下命令编译该程序:

$> gcc -g -o test test.c

/tmp/ccyQ5blC.o: In function `main':
/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open'
/home/tonybai/tmp/test.c:33: undefined reference to `libiconv'
/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'

咦,这是咋搞的呢?怎么找不到iconv库的符号!!!显式加上iconv的链接指示再试试。

$> gcc -g -o test test.c -liconv

这回编译OK了。的确如那位同事所说出现了怪异的情况。

二、现场取证

惯性思维让我首先提出疑问:难道是这台机器上的libc版本有差异,检查一下libc中是否定义了iconv相关符号。

$ nm /lib64/libc.so.6 |grep iconv
000000397141e040 T iconv
000000397141e1e0 T iconv_close
000000397141ddc0 T iconv_open

iconv的函数都定义了呀!怎么会链接不到?

我们再来看看已经编译成功的那个test到底连接到哪个iconv库了。

$ ldd test
    linux-vdso.so.1 =>  (0x00007fff77d6b000)
    libiconv.so.2 => /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)
    libc.so.6 => /lib64/libc.so.6 (0×0000003971400000)
    /lib64/ld-linux-x86-64.so.2 (0×0000003971000000)

哦,系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了,但gcc怎么会链接到这里了呢?

三、大侦探的分析^_^

Gcc到底做了什么呢?我们看看其verbose的输出结果。

$ gcc -g -o test test.c -liconv -v
使用内建 specs。
目标:x86_64-redhat-linux
配置为:../configure –prefix=/usr –mandir=/usr/share/man –infodir=/usr/share/info –enable-shared –enable-threads=posix –enable-          checking=release –with-system-zlib –enable-__cxa_atexit –disable-libunwind-exceptions –enable-libgcj-multifile –enable-languages=c,c++,   objc,obj-c++,java,fortran,ada –enable-java-awt=gtk –disable-dssi –disable-plugin –with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre –with-cpu=generic –host=x86_64-redhat-linux
线程模型:posix
gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)
 /usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/     ccypZm0v.s
忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。
GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)
    由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。
GGC 准则:–param ggc-min-expand=100 –param ggc-min-heapsize=131072
Compiler executable checksum: ef754737661c9c384f73674bd4e06594
 as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s
GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020
 /usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 –eh-frame-hdr -m elf_x86_64 –hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.  2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/   lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64
-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc –as-needed -lgcc_s –no-as-needed -lc -lgcc –as-needed -lgcc_s –no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o

从这个结果来看,gcc在search iconv.h这个头文件时,首先找到的是/usr/local/include/iconv.h,而不是/usr/include/iconv.h。这两个文件有啥不同么?

在/usr/local/include/iconv.h中,我找到如下代码:


#ifndef LIBICONV_PLUG
#define iconv_open libiconv_open
#endif
extern iconv_t iconv_open (const char* tocode, const char* fromcode);

libiconv_open vs iconv_open,卧槽!!!再对比一下前面编译时输出的错误信息:

/tmp/ccyQ5blC.o: In function `main':
/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open'
/home/tonybai/tmp/test.c:33: undefined reference to `libiconv'
/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'

大侦探醒悟了!大侦探带你还原一下真实情况。

我们在执行gcc -g -o test test.c时, 根据gcc -v中include search dir的顺序,gcc首先search到的是/usr/local/include/iconv.h,而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数,而这些函数符号显然在libc中是无法找到的,libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了,没有细致查看gcc的编译错误信息中的内容,这就是问题所在!

gcc -g -o test test.c -liconv为何可以顺利编译通过呢?gcc是如何找到/usr/local/lib下的libiconv的呢?大侦探再次为大家还原一下真相。

我们在执行gcc -g -o test test.c -liconv时,gcc同 样首先search到的是/usr/local/include/iconv.h,然后编译test.c源码,ok;接下来启动ld程序进行链接;ld找 到了libiconv,ld是怎么找到iconv的呢,libiconv在/usr/local/lib下,ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径:

$> ld -verbose|grep SEARCH
SEARCH_DIR("/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("/usr/local/lib64"); SEARCH_DIR("/lib64"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");

ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的),因此找到libiconv就不足为奇了。

四、问题解决

我们不想显式的加上-liconv,那如何解决这个问题呢?我们是否可以强制gcc先找到/usr/include/iconv.h呢?我们先来做个试验。

$ gcc -g -o test test.c -liconv -I ~/include -v

忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /home/tonybai/include
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。

试验结果似乎让我们觉得可行,我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。

$ gcc -g -o test test.c -I ~/include -v

忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
忽略重复的目录“/usr/include”
  因为它是一个重复了系统目录的非系统目录
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。

糟糕!/usr/include被忽略了!还是从/usr/local/include开始,方案失败。

似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉!那就这么做吧^_^!

C,C++开源项目中的100个Bugs

5 Comments

俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目,诸如Apache Http ServerChromiumClangCMakeMySQL等的源码进行了分析,找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析,倒不如在编码时就提高自知自觉,避免这份列表上的错误发生在你的代码中,因此这里将部分摘录一些Bugs(Bug编号这里不连续,为的是对应原文的编号)并做简要说明。原文将这份Bug列表分为了几类,这里也将沿用这个思路。

一、数组和字符串处理错误

数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。

[#1] Wolfenstein 3D项目 -"只有部分对象被clear了"

void CG_RegisterItemVisuals( int itemNum ) {
    …
    itemInfo_t *itemInfo;
    …
    memset( itemInfo, 0, sizeof( &itemInfo ) );
    …
}

这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存,但调用memset时,第三个参数传入的却是sizeof(&iteminfo),要知道 sizeof(&itemInfo) != sizeof(itemInfo_t),前者只是一个指针的大小罢了。正确的写法是:

memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));

[#2] Wolfenstein 3D项目 -"只有部分Matrix被clear了"

ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {
    memcpy( mat, src, sizeof( src ) );
}

这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个二维数组。但这里有个坑:那就是作为函数形式参数的数组名已经退化为指针了,对其sizeof只能得到一个指针的长度,因此这里的 memcpy只是copy了一个指针的长度,没有copy全。这里的代码是C++代码,原文中给出了正确的改正方法 – 传reference:

ID_INLINE mat3_t::mat3_t( float (&src)[3][3] )
{
    memcpy( mat, src, sizeof( src ) );
}

[#4] ReactOS项目 – "错误地计算一个字符串的长度"

static const PCHAR Nv11Board = "NV11 (GeForce2) Board";
static const PCHAR Nv11Chip = "Chip Rev B2";
static const PCHAR Nv11Vendor = "NVidia Corporation";

BOOLEAN
IsVesaBiosOk(…)
{
    …
    if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &&
            !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &&
            !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &&
            (OemRevision == 0×311))
    …
}

Bug处在IsVesaBiosOK中那一串strncmp调用中,代码将一个指针的size传入strncmp作为第三个参数,导致 strncmp实际只是比较了字符串的前4 or 8个字节,而不是字符串的全部内容。

[#6] CPU Identifying Tool项目 – 数组越界

#define FINDBUFFLEN 64  // Max buffer find/replace size

int WINAPI Sticky (…)
{
    …
    static char findWhat[FINDBUFFLEN] = {'\0'};
    …
    findWhat[FINDBUFFLEN] = '\0';
    …
}

bug出在"findWhat[FINDBUFFLEN] = ‘\0′;”这一行。数组的最大长度为FINDBUFFLEN,但下标的最大值应该是FINDBUFFLEN-1,而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = '\0';

[#7] Wolfenstein 3D项目 – 数组越界

typedef struct bot_state_s
{
    …
    char teamleader[32]; //netname of the team leader
    …
}  bot_state_t;

void BotTeamAI( bot_state_t *bs ) {
    …
    bs->teamleader[sizeof( bs->teamleader )] = '\0';
    …
}

"sizeof( bs->teamleader )]"这行的结果值已经超出了数组的最大边界,正确的代码是:

bs->teamleader[
  sizeof(bs->teamleader) / sizeof(bs->teamleader[0]) – 1
  ] = '\0';

[#8] Miranda IM项目 – 只Copy了部分字符串

struct _textrangew
{
    CHARRANGE chrg;
    LPWSTR lpstrText;
} TEXTRANGEW;

const wchar_t* Utils::extractURLFromRichEdit(…)
{
    …
    ::CopyMemory(tr.lpstrText, L"mailto:", 7);
    …
}

这里的bug在于L"mailto:"是宽字符串,宽字符串中的每个字符占2或4个字节(依Compiler使用的字符集编码而定),因此这里只 copy 7个字节显然是不够的,应该是7 * sizeof(wchar_t)。

[#9] CMake项目 – 循环內的数组越界

static const struct {
    DWORD   winerr;
    int     doserr;
} doserrors[] =
{
    …
};

static void
la_dosmaperr(unsigned long e)
{
    …
    for (i = 0; i < sizeof(doserrors); i++)
    {
        if (doserrors[i].winerr == e)
        {
            errno = doserrors[i].doserr;
            return;
        }
    }
    …
}

作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数,但sizeof(doserrors)返回的却是数组占用的字节个数,这远远大于数组元素个数,因此造成数组越界。正确的写法:

for (i = 0; i < sizeof(doserrors) / sizeof(*doserrors); i++)

[#10] CPU Identifying Tool项目 – 打印到自身的字符串

char * OSDetection ()
{
    …
    sprintf(szOperatingSystem,
                    "%sversion %d.%d %s (Build %d)",
                    szOperatingSystem,
                    osvi.dwMajorVersion,
                    osvi.dwMinorVersion,
                    osvi.szCSDVersion,
                    osvi.dwBuildNumber & 0xFFFF);
    …
    sprintf (szOperatingSystem, "%s%s(Build %d)",
                      szOperatingSystem, osvi.szCSDVersion,
                      osvi.dwBuildNumber & 0xFFFF);
    …
}

通过sprintf,szOperatingSystem字符串将自己打印到自己里面,这是十分危险的,将导致无法预知的错误结果,可能会导致栈溢出等严重问题。

[#12] Notepad++项目 – 数组局部clear

#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];

DockingManager::DockingManager()
{
    …
    memset(_iContMap, -1, CONT_MAP_MAX);
    …
}

代码的原本试图将数组_iContMap清零,但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小,而只是数组的元素个数而已,显然其忘记乘以sizeof(int)了。

二、未定义行为

在C/C++的语言规范中,我们常常能看到“xx is undefined”。规范中并没有明确表明这类错误是什么样子的,只是说取决于Compiler的实现,也许Compiler会给出正确的结果,但这么使用却是不可移植的。

[#1] Chromium项目 – 智能指针的误用

void AccessibleContainsAccessible(…)
{
    …
    auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
    …
}

这里的问题在于使用new[]分配的内存,在智能指针释放时却用了delete,这将会导致未定义行为。看看autoptr的destructor就知道了:

~auto_ptr() {
    delete _Myptr;
}

我们可以找一些更合适的类来fix这个问题,比如boost::scopedarray。

[#2] IPP Sample项目 – 经典未定义行为

template<typename T, Ipp32s size> void HadamardFwdFast(…)
{
  Ipp32s *pTemp;
  …
  for(j=0;j<4;j++) {
    a[0] = pTemp[0*4] + pTemp[1*4];
    a[1] = pTemp[0*4] – pTemp[1*4];
    a[2] = pTemp[2*4] + pTemp[3*4];
    a[3] = pTemp[2*4] – pTemp[3*4];
    pTemp = pTemp++;
    …
  }
  …
}

很多人一眼就看到了"pTemp = pTemp++"这行,对于这个代码编译器会产生两种结果截然不同的翻译:

pTemp = pTemp + 1;
pTemp = pTemp;

TMP = pTemp;
pTemp = pTemp + 1;
pTemp = TMP;

到底是哪种呢?依赖于编译器的实现,甚至是优化级别的设定。

三、与运算优先级相关的错误

[#1] MySQL工程 – !和&的运算优先级

int ha_innobase::create(…)
{
  …
  if (srv_file_per_table
            && !mysqld_embedded
            && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
  …
}

这段代码原意是想测试create_info->options变量中几个bit位的值是否set了,即!(create_info->options & HA_LEX_CREATE_TMP_TABLE),但由于!的运算优先级高于&,实际逻辑变成了(!create_info->options) & HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作,就不要吝啬小括号了。

[#2] Emule工程 – *和++的运算优先级

STDMETHODIMP
CCustomAutoComplete::Next(…, ULONG *pceltFetched)
{
  …
  if (pceltFetched != NULL)
    *pceltFetched++;
  …
}

显然作者原意是想对pceltFetched所指向的long型变量进行++操作,但由于*和++的运算优先级没有搞对,导致实际上执行了*(pceltFetched++)的操作,而不是(*pceltFetched)++操作。

[#3] Chromium项目 – &和!=的运算优先级

#define FILE_ATTRIBUTE_DIRECTORY 0×00000010

bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
  …
  info->is_directory =
    file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
  …
}

这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录,逻辑上应该是(file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0,但由于!=优先级高于&,原代码中无括号,结果逻辑变成了file_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY != 0),导致is_directory将永远求值为true。

[#4] BCmenu项目 – if和else弄混

void BCMenu::InsertSpaces(void)
{
  if(IsLunaMenuStyle())
    if(!xp_space_accelerators) return;
  else
    if(!original_space_accelerators) return;
  …
}

这又是C语言的一个“大坑”,无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看,else似乎是与最外层的if配对使用,但实际这段代码的效果是:

if(IsLunaMenuStyle())
{
   if(!xp_space_accelerators) {
     return;
   } else {
     if(!original_space_accelerators) return;
   }
}

这显然不是程序员原意,看来括号必要时还是不能省略的。修改后的代码如下:

if(IsLunaMenuStyle()) {
  if(!xp_space_accelerators) return;
} else {
  if(!original_space_accelerators) return;
}

四、格式化输出错误

[#1] ReactOS项目 – 错误地输出WCHAR字符

static void REGPROC_unescape_string(WCHAR* str)
{
  …
  default:
    fprintf(stderr,
            "Warning! Unrecognized escape sequence: \\%c'\n",
            str[str_idx]);
  …
}

%c是用来格式化输出非宽字符的,这里用来输出WCHAR显然会得到错误的结果,fix solution是将%c换位%C。

[#2] Intel AMT SDK项目 – 缺少%s

void addAttribute(…)
{
  …
  int index = _snprintf(temp, 1023, 
                        "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
                        "%02x%02x:02x%02x:%02x%02x:%02x%02x",
                        value[0],value[1],value[2],value[3],value[4],
                        value[5],value[6],value[7],value[8],
                        value[9],value[10],value[11],value[12],
                        value[13],value[14],value[15]);
  …
}

 

不解释了,自己慢慢数和对照吧。

[#3] Intel AMT SDK项目 – 未使用的参数

bool GetUserValues(…)
{
  …
  printf("Error: illegal value. Aborting.\n", tmp);
  return false;
}

显然tmp是多余的。

五、书写错误

[#1] Miranda IM项目 – 在if中赋值

void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)
{
  …
  else if (wTLVType = 0×29 && wTLVLen == sizeof(DWORD))
  …
}

“wTLVType = 0×29”显然是笔误,应该是“wTLVType == 0×29”才对。

[#3] Clang项目 – 对象名书写错误

static Value *SimplifyICmpInst(…) {
  …
  case Instruction::Shl: {
    bool NUW =
      LBO->hasNoUnsignedWrap() && LBO->hasNoUnsignedWrap();
    bool NSW =
      LBO->hasNoSignedWrap() && RBO->hasNoSignedWrap();
  …
}

从最后一行先后使用了LBO和RBO来看,前面只用了LBO的那行很可能是有问题的,正确的应该是:

bool NUW =
      LBO->hasNoUnsignedWrap() && RBO->hasNoUnsignedWrap();

[#6] G3D Content Pak项目 – 一对括号放错了地方

bool Matrix4::operator==(const Matrix4& other) const {
  if (memcmp(this, &other, sizeof(Matrix4) == 0)) {
    return true;
  }
  …
}

由于括号放错了地方,导致memcmp最后的参数变成了sizeof(Matrix4) == 0,这行代码的正确写法应该是:

if (memcmp(this, &other, sizeof(Matrix4)) == 0) {

[#8] Apache Http Server项目 – 多余的sizeof

PSECURITY_ATTRIBUTES GetNullACL(void)
{
  PSECURITY_ATTRIBUTES sa;
  sa  = (PSECURITY_ATTRIBUTES)
    LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));
  sa->nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));
  …
}

最后一行显然是笔误,sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。

[#10] Notepad++项目 – 在本来应该用&的地方使用了&&

TCHAR GetASCII(WPARAM wParam, LPARAM lParam)
{
  …
  result=ToAscii(wParam,
                 (lParam >> 16) && 0xff, keys,&dwReturnedValue,0);
  …
}

(lParam >> 16) && 0xff没有什么意义,求值结果总是true。这里的代码应该是(lParam >> 16) & 0xff。

[#12] Fennec Media Project项目 – 额外的分号

int settings_default(void)
{
  …
  for(i=0; i<16; i++);
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的:

for(i=0; i<16; i++) 
{
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

但实际执行代码逻辑却是:

for(i=0; i<16; i++) 
{
    ;
}

for(j=0; j<32; j++)
{   
  settings.conversion.equalizer_bands.boost[i][j] = 0.0;
  settings.conversion.equalizer_bands.preamp[i]   = 0.0;
}

这一切都是那个;导致的。

六、对基本函数和类的误用

[#2] TortoiseSVN项目 – remove函数的误用

STDMETHODIMP CShellExt::Initialize(….)
{
  …
  ignoredprops = UTF8ToWide(st.c_str());
  // remove all escape chars ('\\')
  std::remove(ignoredprops.begin(), ignoredprops.end(), '\\');
  break;
  …
}

作者意图删除所有'\\',但他用错了函数,remove函数只是交换元素的位置,将要删除的元素交换到尾部trash,并且返回指向trash首地址的iterator。正确的做法应该是"v.erase(remove(v.begin(), v.end(), 2), v.end())"。

[#5] Pixie项目 – 在循环中使用alloca函数

inline  void  triangulatePolygon(…) {
  …
  for (i=1;i<nloops;i++) {
    …
    do {
      …
      do {
        …
        CTriVertex  *snVertex =
         (CTriVertex *)alloca(2*sizeof(CTriVertex));
        …
      } while(dVertex != loops[0]);
      …
    } while(sVertex != loops[i]);
    …
  }
  …
}

alloca函数在栈上分配内存,因此在循环中使用alloca可能会很快导致栈溢出。

七、无意义的代码

[#1] IPP Samples项目 – 不完整的条件

void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],
                                 Ipp32s* mask, Ipp32s len)
{
  Ipp32s  i;
  Ipp32f  norm;

  for(i=0; i<len; i++) {
    if(mask<0) continue;
    norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+
                     vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);
    vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;
  }
}

mask是Ipp32s类型指针,这样if (mask< 0)这句代码显然没啥意义,正确的代码应该是:

if (mask[i] < 0) continue;

[#2] QT项目 – 重复的检查

Q3TextCustomItem* Q3TextDocument::parseTable(…)
{
  …
  while (end < length
         && !hasPrefix(doc, length, end, QLatin1String("</td"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</th"))
         && !hasPrefix(doc, length, end, QLatin1String("<th"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</tr"))
         && !hasPrefix(doc, length, end, QLatin1String("<tr"))
         && !hasPrefix(doc, length, end, QLatin1String("</table"))) {

  …
}

这里对"<td"做了两次check。

八、总是True或False的条件

[#1] Shareaza项目 – char类型的值范围

void CRemote::Output(LPCTSTR pszName)
{

  …
  CHAR* pBytes = new CHAR[ nBytes ];
  hFile.Read( pBytes, nBytes );
  …
  if ( nBytes > 3 && pBytes[0] == 0xEF &&
             pBytes[1] == 0xBB && pBytes[2] == 0xBF )
  {
    pBytes += 3;
    nBytes -= 3;
    bBOM = true;
  }
  …
}

表达式"pBytes[0] == 0xEF"总是False。char类型的值范围是-128~127 < 0xEF,因此这个表达式总是False,导致整个if condition总是为False,与预期逻辑不符。

[#3] VirtualDub项目 – 无符号类型总是>=0

typedef unsigned short wint_t;

void lexungetc(wint_t c) {
  if (c < 0)
    return;
   g_backstack.push_back(c);
}

c是unsigned short类型,永远不会小于0,也就是说if (c < 0)永远为False。

[#8] MySQL项目 – 条件错误

enum enum_mysql_timestamp_type
str_to_datetime(…)
{
  …
  else if (str[0] != ‘a’ || str[0] != 'A')
    continue; /* Not AM/PM */
  …
}

if (str[0] != ‘a’ || str[0] != 'A')这个条件永远为真。也许这块本意是想用&&。

九、代码漏洞

导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类,因为入侵者可以利用这些错误来攻击你的代码,获取其利益。

[#1] Ultimate TCP/IP项目 – 空字符串的错误检查

char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)
{
  …
  if (m_szPassword != NULL)
  {
    …
    if (m_szPassword != '\0')
    {
  …
}

第二个if condition check意图检查m_szPassword是否为空字符串,但却错误的将指针与'\0'进行比较,正确的代码应该是这样的:

if (*m_szPassword != '\0')

[#2] Chromium项目 – NULL指针的处理

bool ChromeFrameNPAPI::Invoke(…)
{
  ChromeFrameNPAPI* plugin_instance =
    ChromeFrameInstanceFromNPObject(header);
  if (!plugin_instance &&
      (plugin_instance->automation_client_.get()))
    return false;
  …
}   

一旦plugin_instance为NULL,!plugin_instance为True,代码对&&后面的子条件求值,引用plugin_instance将导致程序崩溃。正确的做法应该是:

if (plugin_instance &&
        (plugin_instance->automation_client_.get()))
  return false;

[#5] Apache httpd Server项目 – 不完整的缓冲区clear

#define MEMSET_BZERO(p,l)       memset((p), 0, (l))

void apr__SHA256_Final(…, SHA256_CTX* context) {
  …
  MEMSET_BZERO(context, sizeof(context));
  …
}

这个错误前面提到过,sizeof(context)只是指针的大小,将之改为sizeof(*context)就OK了。

[#7] PNG Library项目 – 意外的指针clear

png_size_t
png_check_keyword(png_structp png_ptr, png_charp key,
                    png_charpp new_key)
{
  …
  if (key_len > 79)
  {
    png_warning(png_ptr, "keyword length must be 1 – 79 characters");
    new_key[79] = '\0';
    key_len = 79;
  }
  …
}

new_key的类型为png_charpp,顾名思义,这是一个char**类型,但代码中new_key[79] = ‘\0′这句显然是要给某个char赋值,但new_key[n]得到的应该是一个地址,给一个地址赋值为’\0′显然是有误的。正确的写法应该是(*new_key)[79] = '\0'。

[#10] Miranda IM项目 – 保护没生效

void Append( PCXSTR pszSrc, int nLength )
{
  …
  UINT nOldLength = GetLength();
  if (nOldLength < 0)
  {
    // protects from underflow
    nOldLength = 0;
  }
  …
}

nOldLength椒UINT类型,其值永远不会小于0,因此if (nOldLength < 0)这行成了摆设。

[#12] Ultimate TCP/IP项目 – 不正确的循环结束条件

void CUT_StrMethods::RemoveSpaces(LPSTR szString) {
  …
  size_t loop, len = strlen(szString);
  // Remove the trailing spaces
  for(loop = (len-1); loop >= 0; loop–) {
    if(szString[loop] != ' ')
      break;
  }
  …
}

循环中的结束条件loop >= 0将永远为True,因为loop变量的类型是size_t是unsigned类型,永远不会小于0。

十、拷贝粘贴

和笔误不同,程序员们决不因该低估拷贝粘贴问题,这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。

[#1] Fennec Media Project项目 – 处理数组元素时出错

void* tag_write_setframe(char *tmem,
                         const char *tid, const string dstr)
{
  …
  if(lset)
  {
    fhead[11] = '\0';
    fhead[12] = '\0';
    fhead[13] = '\0';
    fhead[13] = '\0';
  }
  …
}

 

咋看一下,fhead[13]做了两次赋值,似乎没啥问题。但仔细想一下,最后那行程序员的原意极可能是想写fhead[14] = '\0'。问题就在这里了。

[#2] MySQL项目 – 处理数组元素时出错

static int rr_cmp(uchar *a,uchar *b)
{
  if (a[0] != b[0])
    return (int) a[0] – (int) b[0];
  if (a[1] != b[1])
    return (int) a[1] – (int) b[1];
  if (a[2] != b[2])
    return (int) a[2] – (int) b[2];
  if (a[3] != b[3])
    return (int) a[3] – (int) b[3];
  if (a[4] != b[4])
    return (int) a[4] – (int) b[4];
  if (a[5] != b[5])
    return (int) a[1] – (int) b[5];
  if (a[6] != b[6])
    return (int) a[6] – (int) b[6];
  return (int) a[7] – (int) b[7];
}

 

编写这类代码时,我猜绝大多数人会选择Copy-Paste,然后再逐行修改,问题就发生在修改过程中,上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了:return (int) a[1] – (int) b[5];显然这里的正确代码应该是return (int) a[5] – (int) b[5]。

[#3] TortoiseSVN项目 文件名不正确

BOOL GetImageHlpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,               
                                dwLS)) ;            
}

BOOL GetDbgHelpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,                           
                                dwLS)) ;                        
}

GetImageHlpVersion和GetDbgHelpVersion都使用了"DBGHELP.DLL"文件,显然GetImageHlpVersion写错文件名了。应该用"IMAGEHLP.DLL"就对了。

[#4] Clang项目 – 等同的函数体

MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;

void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}

void clearTopDownPointers() {
  PerPtrTopDown.clear();
}

我们看到虽然两个函数名不同,但是函数体的内容是相同的,显然又是copy-paste惹的祸。做如下修改即可:

void clearBottomUpPointers() {
  PerPtrBottomUp.clear();
}

 

十一、Null指针的校验迟了

这里的“迟了”的含义是先使用指针,然后再校验指针是否为NULL。

[#1] Quake-III-Arena项目 – 校验迟了

void Item_Paint(itemDef_t *item) {
  vec4_t red;
  menuDef_t *parent = (menuDef_t*)item->parent;
  red[0] = red[3] = 1;
  red[1] = red[2] = 0;
  if (item == NULL) {
    return;
  }
  …
}

 

在校验item是否为NULL前已经使用过item了,一旦item真的为NULL,那程序必然崩溃。

十二、其他杂项

[#1] Image Processing 项目 – 八进制数

inline
void elxLuminocity(const PixelRGBus& iPixel,
                     LuminanceCell< PixelRGBus >& oCell)
{
  oCell._luminance = uint16(0.2220f*iPixel._red +
                            0.7067f*iPixel._blue + 0.0713f*iPixel._green);
  oCell._pixel = iPixel;
}

inline
void elxLuminocity(const PixelRGBi& iPixel,
                     LuminanceCell< PixelRGBi >& oCell)
{
  oCell._luminance = 2220*iPixel._red +
    7067*iPixel._blue + 0713*iPixel._green;
  oCell._pixel = iPixel;
}

第二个函数,程序员原意是使用713这个十进制整数,但0713 != 713,在C中,0713是八进制的表示法,Compiler会认为这是个八进制数。

[#2] IPP Sample工程 – 一个变量用于两个loop中

JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)
{
  …
  for(c = 0; c < m_scan_ncomps; c++)
  {
    block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));

    // skip any relevant components
    for(c = 0; c < m_ccomp[m_curr_comp_no].m_comp_no; c++)
    {
      block += (DCTSIZE2*m_ccomp

[/c]

.m_nblocks);
    }
  …
}

变量c用在了两个loop中,这会导致只有部分数据被处理,或外部循环中止。

[#3] Notepad++项目 – 怪异的条件表达式

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT == L_PHP)
    return -1;
  …
}

代码中的那行if条件等价于 if (langT == L_PHP),显然似乎不是作者原意,猜测正确的代码应该是这样的:

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT != L_PHP)
    return -1;
  …
}

Hello,Sublime Text 2

1 Comment

用惯了Vim后,也会有一种尝试新Editor的冲动,这回Sublime Text 2满足了我的这个需求。据说Sublime Text是目前最火的代码编辑器之一,我周围为数不多的几个比较Geek的同事都已经开始使用Sublime Text 2或用了很长时间了,其官方网站首页的Feature Demo也的确非常地炫。

安装Sublime Text 2

我的实验环境Ubuntu 12.04.1 32-bit Desktop版,默认Ubuntu Unity桌面,iBus拼音输入法。

Sublime Text 2的安装极其简单,遵循着download(http://www.sublimetext.com/2) -> unzip -> add path -> start and use的经典路线。我下载的Sublime Text 2是2.0.1版本,启动后一切正常。

安装后目录结构

安装后的Sublime Text 2的目录结构非常简洁:

$ ls
Icon/  PackageSetup.py   Pristine Packages/
lib/   sublime_plugin.py   sublime_text*

lib下是自带的Python26环境;Pristine Packages下是各种编程语言的插件包。

在我的环境下Sublime Text 2的用户配置与包环境放在了~/.config/sublime-text-2/下面,

$ ls
Installed Packages/  Packages/  Pristine Packages/  Settings/

这里面最重要的目录就是Packages目录了,这里是Sublime Text 2用第三方包扩展自身Feature的包存储路径。

安装package control

package control包之于Sublime Text 2就好比apt工具之于Ubuntu,它是一个方便第三方包安装、卸载和管理的第三方包。在其官网(http://wbond.net/sublime_packages/package_control)上明示了其安装方法:

* 敲入 ctrl + ` 调出命令行窗口
* 在命令行窗口中输入下面的代码,回车执行。

import urllib2,os; pf='Package Control.sublime-package'; ipp=sublime.installed_packages_path(); os.        makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2.   ProxyHandler())); open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()); print('Please restart Sublime Text to finish installation')

* 重启Sublime Text 2。

注意:如果需要代理访问外网的话,需要正确设置http_proxy环境变量。

敲入"ctrl + shift + p"可打开命令窗口,输入"Package Control",你会看到窗口下拉提示中Package Control支持的功能,常用的我们会选择:“Package Control: Install Package”。

安装中文支持

中国程序员每每在尝试一种国外程序员新开发的编辑器时,都会遇到中文字符集编码的问题,这次Sublime Text 2也不例外,它原生就不支持中文显示。还好中国程序员是无比聪明的,开发了ConvertToUTF8这样的第三方包,让我们可以看到中文并用中文编辑。

最简单的安装ConvertToUTF8的方法就是用Package Control安装,选择Package Control: Install Package后,搜素ConvertToUTF8,找到后,点击即可安装。安装后,你会在~/.config/sublime-text-2/Packages下面看到ConvertToUTF8包目录。

再次启动Sublime Text 2后,打开一个GBK编码的中文文档,居然提示ConvertToUTF8工作不正常。后发现ConvertToUTF8主页上有提示,Python 2.6下的ConvertToUTF8需要一个Codecs26的Package才能正常运行。下载Codecs26后,解压安装到Packages下面,重新启动Sublime Text 2,Sublime Text 2直接dump core。从Packages目录下将Codecs26删除后,Sublime Text 2恢复正常。

又细致读了ConvertToUTF8作者的README文件,发现master branch上的Codecs26是for 64位版本的,我需要下载x32 branch上的包。的确,下载并安装x32 branch上的Codecs26后,Sublime Text 2启动OK,转换中文OK了。

注意:不要与其他支持GBK转换的包(比如GBK Encoding Support)混用,否则ConvertToUTF8无法works。

解决中文输入问题

好不容易能看GBK编码的中文文件了,却发现无法输入中文,无论如何切换输入法和重启输入法,都无法输入中文。网上介绍可通过"Input Helper Package(cd .config/sublime-text-2/Packages; git clone http://github.com/xgenvn/InputHelper.git)"解决问题。问题的确可以解决,不过输入中文时太麻烦了:需要先敲入"ctrl+shift+z"调出中文输入框,再在这个框里输入中文。

网上都说这是iBus输入法与Sublime Text 2的兼容问题,要想解决就要换fcitx。以前用过fcitx感觉默认输入法比较弱,不过现在fcitx有google pinyin了,体验一定会提高不少。通过下面命令一键安装fcitx:

sudo apt-get install fcitx fcitx-googlepinyin

安装后,在“语言支持”中用fcitx替换掉iBus。在“启动应用程序”中加入:

名称: Fcitx
命令: /usr/bin/fcitx -d
注释t: Fcitx启动

注销再登录后,再打开Sublime Text 2,终于可以输入中文了。

功能

用了一遭儿,Sublime Text 2最吸引我的Feature包括:“Goto Anything”和“Multi-Selection”。在一个工程中,通过ctrl + p调出一个输入框,Sublime Text 2首先在文件名级别对你输入的文本进行匹配;待选择好文件后,继续输入@,可看到下拉列表中显示这个文件中所有函数名的名称列表;如果输入的是#,那么下拉列表中将显示该文件中的所有符号。选择某个函数名或符号后,光标将停留在某个符号上,这时我们可以用Multi-Selection这个功能了,如果你要将这个文件中同名符号全选出来,直接Alt+F3即可;如果要选择接下来的N个同名符号,那么敲入N次ctrl + D即可。

不过要想实现ctags那种在符号上跳转到符号定义或符号调用者的功能,Sublime Text 2还无法原生支持,可考虑安装Sublime Text 2的Ctags插件实现:直接在Packages目录下git clone https://github.com/SublimeText/CTags.git。之后:
- “ctrl +t, ctrl+ r"会重新生成tags文件(前提:系统内安装了ctags程序)
- "ctrl +t, ctrl + t"会跳到光标所在符号的定义处;
- "ctrl + t, ctrl + b"会跳回上次的位置;

感受

Sublime Text 2给我的最大感受就是“快”!你在搜索、切换符号、选择文件列表中文件或符号的同时,整个文件会同步的展现你的屏幕上。

简析指针与多维数组

4 Comments

上一篇文章中对多级指针做了简要分析,其实只有当指针与多维数组以及函数联合在一起使用时,麻烦才算真正到来。

零、数组与数组名

C语言中的数组的一般声明形式如下:

T arr_name[n]; /* T为类型,n为数组元素个数 */

内存布局角度来说,数组T arr_name[n]就是内存中连续的内存单元,每个内存单元的长度为sizeof(T),数组的起始内存单元地址为arr_name所在的内存地址, 同时也是数组第一个元素arr_name[0]的内存地址。

C语言数组的数组名(arr_name)有这样的特点:arr_name = &arr_name = *arr_name = 数组起始地址。见下面例子:

char a[5];

printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("*a = %p\n", *a);

输出结果:

a = 0xbfb146c0
&a = 0xbfb146c0
*a = 0xbfb146c0

C语言数组与指针有着紧密的联系。数组名本身的值就是数组的起始地址,有了地址,就有了指针存在的理由了。

1) 数组名可以被当作指针来用

    char a[5] = {1, 2, 3, 4, 5};
    printf("%d, %d, %d\n", *a, *(a+1), *(a+2)); // 输出1, 2, 3

   
    这种用法下,数组名相当于指向数组首地址的char*指针变量。

2) 数组名可以作为地址被赋值给兼容类型的指针变量
   
    char a[5] = {1, 2, 3, 4, 5};
    char *p = a;
    printf("%d, %d, %d\n", *p, *(p+1), *(p+2)); //输出1, 2, 3

3) 数组名不可以被当作指针变量来赋值

    char a[5] = {1, 2, 3, 4, 5};
    char b[5] = {6, 7, 8, 9, 0};

    a = b; //编译器提示错误:将‘char *’赋值给‘char[5]’时类型不兼容

    数组名与指针变量不同:指针变量有单独的存储空间,其存储空间内存储的是指向的内存单元的地址,但数组名只是个"代号"而已,其没有单独的存储空间,其所 在内存地址中存储的是数组第一个元素的元素值,而不是一个地址。或者说数组名代表的是一个值类型,char a[5]中的a可理解为是一个char[5]的值类型变量。将一个数组指针变量值赋值给一个值变量显然是不合逻辑的,也是非法的。

4) 考虑到效率,数组无法被按值传递给函数
   
    虽然数组名可以理解为一个值类型变量,但将数组名传递给函数时,传递的不是数组的全部,而只是数组的首地址,这显然是有效率方面考虑的。如果是传递数组的 全部,那碰到大数组时,这个mem copy的效率显然是不可接受的。但通过这个首地址,函数内部也是可以访问和修改数组中的所有元素的。
   
5) 函数形参中的数组变量将被转化为兼容类型指针变量对待

正如4)中所言,数组是以传址方式传入函数的。对于以数组变量作为形参的函数来说,在函数内部引用该参数时,会自动将该参数视为数组类型兼容的指 针变量,比如:
    char a[5] = {1, 2, 3, 4, 5};

    void foo(char a[5]) {
        printf("sizeof(a) = %d\n", sizeof(a));
    }

    这是一个经典的C语言“陷阱”。foo形参中变量a已经转化为一个char*类型指针了。对该指针变量进行sizeof操作,所得的 size仅是一个指针的长度(在32bit编译下是4),而不是a数组的长度(4 * 5)。

一、多维数组的理解

C语言中管数组的数组(的数组的…)称为多维数组,虽然高于二维的多维数组并不经常使用和遇见。

T multi_arr_name[i][j][k];

多维数组也是数组,根据数组的理解,多维数组也是内存中连续分配的内存单元,只是这些物理分配的内存单元被从逻辑上看成是“行”、“列”以及各种 维度罢了。《C专家编程》中有一种理解方法:将数组看成是一种向量,也就是某种对象的一维数组;当其元素为其他数组时,这个向量也就是我们所说的 多维数组。

我们来结合例子理解一下多维数组,从低维到高维度逐步理解:

1) 一维数组

char a[2];
这是一个向量,拥有两个元素,向量中的元素类型为char。可以理解为:

char a[2]; <=> (char) a[2];

2) 二维数组

char a[2][3];
这是一个向量,拥有两个元素,向量中的元素类型为char[3]。可以理解为:

char a[2][3]; <=> (char[3]) a[2];

3) 三维数组

char a[2][3][5];
这是一个向量,拥有两个元素,向量中的元素类型为char[3][5]。可以理解为:

char a[2][3][5]; <=> (char[3][5]) a[2];

4) N维数组

char a[i][j][k]…[z];
这是一个向量,拥有i个元素,向量中的元素类型为char[j][k]…[z]。可以理解为:

char a[i][j][k]…[z]; <=> (char [j][k]…[z]) a[i];

二、与数组类型兼容的指针类型

假设有下面这样一个数组:

char a[2][3];

我要声明一个可以指向该数组的指针变量,这个声明该如何书写呢?是 char *p[3]还是char (*p)[3]?按照上面对多维数组的理解:

char a[2][3]; <=> char[3] a[2];

这样我们只需构造出一个指向char[3]类型的指针即可,显然这样的指针声明是(char[3]) *p。哦,不对,这样的声明C编译器是不认的,乾坤大挪移!把(char[3])从中间劈开 => char *p[3],这样对么?这个是指向数组a的指针么?怎么越看越像是一个指针数组阿,char *p[3]<=> (char*) p[3]。哇,真的弄错了,改! 对了,刚才忘记了(char[3]) *p中还有一对括号呢,给*p穿上,=> char (*p)[3]。这回没错了,就是它了。

char a[2][3];
char (*p)[3];

p = a; /* 没有什么比这个还正确的了 */

再来一个三维数组的例子,这次简单直白点。

char a[2][3][5];

变形!=> (char[3][5]) a[2];
指针有了 => (char[3][5]) *p => char (*p)[3][5];

有了上面的例子分析,对于更高维度数组,你还不会声明其兼容的指针类型吗?

理解了多维数组兼容的指针变量的类型声明,那么将多维数组与函数结合在一起使用时,你就会得心应手了,在函数内部你看到的、能用到的就是多维数组 对应的兼容指针类型变量。

三、多维数组中的“隐式数组名”

在很多C语言书中,我们会经常看到这样的描述:对于多维数组char a[m][n][h],其中的某个元素a[i][j][k] <=> *(*(*(a + i) + j) + k)。这种等价形式是如何形成的呢?

第零小节的描述告诉我们:数组名是具有指针属性的,除了标准的下标引用方式外,还可以以指针的方式做指针运算以及访问元素,这就是 *(*(*(a + i) + j) + k)是合法的原因。

接下来我们来对*(*(*(a + i) + j) + k)做一次分解分析。鉴于一般形式不易理解和输出结果,我们用一个具体的例子来说明。

    char a[2][3][5] = {
        {
            {1, 2, 3, 4, 5},
            {6, 7, 8, 9, 10},
            {11, 12, 13, 14, 15},
        },

        {
            {21, 22, 23, 24, 25},
            {26, 27, 28, 29, 30},
            {31, 32, 33, 34, 35},
        }
    };

    char (*p)[3][5] = a;
    printf("a[1][2][3] = %d\n”, a[1][2][3]);
    printf("a addr = %p\n", a);
    printf("a + 1 = %p\n", a + 1);
    printf("*(a + 1) = %p\n", *(a + 1));
    printf("*(a + 1) + 2 = %p\n", *(a + 1) + 2);
    printf("*(*(a + 1) + 2) = %p\n", *(*(a + 1) + 2));
    printf("*(*(a + 1) + 2) + 3 = %p\n", *(*(a + 1) + 2) + 3);
    printf("*(*(*(a + 1) + 2) + 3) = %d\n", *(*(*(a + 1) + 2) + 3));

编译这个程序,执行输出:

a[1][2][3] = 34
a addr = 0xbfa0893e
a + 1 = 0xbfa0894d
*(a + 1) = 0xbfa0894d
*(a + 1) + 2 = 0xbfa08957
*(*(a + 1) + 2) = 0xbfa08957
*(*(a + 1) + 2) + 3 = 0xbfa0895a
*(*(*(a + 1) + 2) + 3) = 34

我们以*(*(*(a + 1) + 2) + 3)为例,再根据上面的输出结果,逐步拆解分析。

1) a + 1

a的等价指针类型是char (*p)[3][5]; 因此a + 1这个指针运算的结果相当于在数组a的起始地址开始向后移动sizeof(char [3][5])个字节。从输出结果来看,a + 1 = 0xbfa0894d = 0xbfa0893e + 15 = a addr +15也印证了这点。

2) *(a + 1)

通常指针的解引用操作会得到指针所指内存地址所在存储单元中存储的值。但上面的输出结果让我们产生疑问:

*(a + 1) = 0xbfa0894d == a + 1

在若干年前我的文章《挖掘一下C语言中的多维数组》中曾经探讨过这个问题,当时针对这个问题并未给出答案。这次对此问题我又有了新的认识。还记得我们在开篇中对数组名做的操作以及输出结果么:

char a[5];

a = 0xbfb146c0
&a = 0xbfb146c0
*a = 0xbfb146c0

也是a == *a。而这里同样是*(a + 1) == a + 1。通过这个对比我们得到一个大胆的推论:a + 1也可以看作是一个“数组名”,这是一个隐式数组名。只有这个解释看起来是合理的。

3) *(a + 1) + 2

a + 1这个隐式数组名对应的指针类型是char (*p)[5],因此 *(a+1) +2相当于从a + 1地址的开始再向后移动10(2 x 5)个字节,也就是0xbfa08957,输出结果也印证了这点。

4) *(*(a + 1) + 2)

我们又遇到了一个隐式数组名。*(*(a + 1) + 2) = 0xbfa08957 == *(a + 1) + 2。

5) *(*(a + 1) + 2) + 3

*(a + 1) + 2这个隐式数组名对应的指针类型是char *p,因此*(*(a + 1) + 2) + 3相当于从*(a + 1) + 2开始再向后移动3个字节,也就是0xbfa0895a,注意这个地址所在单元上存储的是一个char值。

6) *(*(*(a + 1) + 2) + 3)

如果将*(*(a + 1) + 2) + 3赋值给char *p,那么*(*(*(a + 1) + 2) + 3)就相当于*p,这个再简单不过了,34就是这个单元存储的char值。

简析多级指针解引用

3 Comments

指针是C语言中公认的最为强大的语法要素,但同时也是最难理解的语法要素,它曾给程序员带来了无数麻烦和痛苦,以致于在C语言之后诞生的很多新兴 语言中我们再也难觅指针的身影了。

下面是一个最简单的C语言指针的例子:
int a = 5;
int *p = &a;

其中p就是一个指针变量。如果C语言中仅仅存在这类指针,那显然指针不会形成“大患”。经常地我们会在代码中看到下面的情形:

int **q = &p;
int ***z = &q;

随着符号'*'个数的增加,C代码的理解复杂度似乎也曾指数级别增长似的。像q、z这样的指向指针的指针(pointer to pointer to …)变量,中文俗称“多级指针”。不过在一些正式的英文C语言教程中,我没能找到其正式的英文说法。在老外的这些书 中,它们多被称为pointer to pointer (to pointer to ….)。多级指针的确是很难理解的,特别当与函数、数组等联合在一起使用时。今天在写代码时恰好撞见了多级指针,于是就打算在这里说说对多级指针以及 其解引用的一些粗浅理解。

指针究竟是啥?

和普通变量想比,指针变量到底有何不同,究竟何为指针(变量)?我们来看一个例子:

int a = 5;
int *p = &a;

printf("a addr = [%p]\n", &a);
printf("a content = [%d]\n", a);
printf("p addr = [%p]\n", &p);
printf("p content = [%p]\n", p);
printf("*p = [%d]\n", *p);

*p = 6;
printf("after modify, *p = [%d]\n", *p);

编译这个小程序并执行,输出结果如下:

a addr = [0xbfb609b8]
a content = [5]
p addr = [0xbfb609bc]
p content = [0xbfb609b8]
*p = [5]
after modify, *p = [6]

通过两个变量的addr,我们可以看到a、p两个变量都是在栈上分配的变量。不同的是普通整型变量a对应的内存单元(a content)中存储的值为整型值5,是一个数值;而变量p对应的内存单元(p content)中存储的值为0xbfb609b8,是变量a的地址,用栈变量简图可以表示如下:

| …      |
|0xbfb609b8| <- &p [0xbfb609bc]
|5         | <- &a [0xbfb609b8]
| …      |

可以看出指针变量的第一个特点是它是一种以存储其他变量地址为目的的变量。一个T类型的指针变量(一级指针)就是一个存储了某T类 型值变量的地址的内存单元。

例子中最后那个输出是对指针的解引用(dereference)操作,指针的解引用操作的结果是得到指针所指的地址上的变量的值。在这个例子中指 针所指到内存地址为0xbfb609b8,也就是a变量的位置,因此*p的结果为变量a的值,即5。因此我们得到指针变量的第二个特点: 通过对指针的解引用,我们可以获得其指向的内存单元所表示的值。

在例子中,我们看到了这行代码 *p = 6,并发现执行这行代码后,a变量的值变为了6。这就是指针的第三个特点:当解引用作左值时,它可以修改其所指内存地址上变量的值。a被修改后的栈变量分布简图:

| …      |
|0xbfb609b8| <- &p [0xbfb609bc]
|6         | <- &a [0xbfb609b8]
| …      |

二级指针

我们再来分析一下下面的示例程序的输出结果。

int a = 5;
int b = 13;
int *p = &a;
printf("*p = %d\n", *p); 
int **q = &p;
(*q) = &b;
printf("*p = %d\n", *p);

根据前面的分析,第一次*p输出时p指向a的地址,对p解引用的结果就是a所在内存单元的值,即5。接下来的代码分析起来就需要谨慎一些了。我们先来看看 int **q = &p这行代码。根据对一级指针的分析,我们可以将int **q理解成(int*) *q,这样q指向的地址就是一个int*型的变量的内存地址,该地址上的值本身也是一个地址值。在这个例子中,(int*) *q = &p; 也就是说q中存储的值就是变量p的地址。通过*q我们可以得到p中存储的地址值(&a);而若*q作为左值,显然就是修改p中存储的地址值喽,因 此(*q) = &b则相当于p = &b,则第二个*p的输出结果为变量b所在内存单元的值,即13。

在修改*q前,栈上内存布局:

| …      |
|0xbf830ec8| <- &q [0xbf830ecc]
|0xbf830ec0| <- &p [0xbf830ec8]
|11        | <- &b [0xbf830ec4]
|5         | <- &a [0xbf830ec0]
| …      |

在修改*q的值后,栈上内存布局:

| …      |
|0xbf830ec8| <- &q [0xbf830ecc]
|0xbf830ec4| <- &p [0xbf830ec8] /* 通过*q修改 */
|11        | <- &b [0xbf830ec4]
|5         | <- &a [0xbf830ec0]
| …      |

再来分析一下**q的值又是啥呢?有了前面的铺垫:*q <=> p,那**q <=> *(*q) <=> *p,其值自然就明了了,就是b的值。

多级指针

有了一级指针和二级指针的分析打基础,当我们遇到更多*的时候,只是遵循这个方法耐心分析就是了,比如:

int a = 5;
int *p = &a;
int **q = &p;
int ***z = &q;

我们可以对比着前面一、二级指针的理解方法来理解这三个指针p、q和z:
    – 一级指针p自身存储的是整型值变量a的地址,对一级指针解引用(*p)得到的是值变量a的值;*p作左值,修改的是变量a的值;
    – 二级指针q自身存储的是一级整型指针变量p的地址,对二级指针解引用(*q)得到的是一级指针p自身存储的值(a的地址:&a);*p作左值时,修改的一级指针p的指向;
    – 三级指针z自身存储的是二级整型指针变量q的地址,对三级指针解引用(*z)得到的是二级指针q自身存储的值,也就是p的地址(&p);对*z再 解引用(**z),相当于得到p自身存储的值,也就是a的地址&a;对**z再解引用,即***z,相当于得到a自身存储的变量值,即5。用一个 等价式可以更形象的表达:***z <=> **(*z) <=> **q <=> *(*q) <=> *p <=> 5。
    – 更高级别的指针可依次类推。不过如果再对***z解引用,即****z,那则相当于对整型数5(非地址)进行解引用,会出现编译错误: 一元 ‘*’参数类型无效(有‘int’)。

Older Entries