一个类型提升的bug

今天碰到一个由于类型提升的bug, 即使找到bug后, 仍花了好大功夫才找到解释, 感觉此坑比较隐蔽, 在此小记一下.

有类似下面一段代码:

unsigned char a;
unsigned char b;

a = -1;
b = 0;
if (a == b - 1)
printf("equal");
else
printf("not equal");

理论上当a为-1, b为0时应该打印出equal. 可事实恰恰相反, 打印出来的是not equal.

C语言中有以下类型提升规则:
2. 一般常数为int型
1. 二元操作符如果两个操作数具有不同的类型, 那么将较低的类型提升为较高的类型, 运算的结果为较高的类型
2. 将char与short类型的操作数转换为int类型

a,b被提升为int.
因为a是无符号型char, 因此a不会被进行符号扩展, 因此会被提升为0x000000ff
因为b是无符号型char, 因此a不会被进行符号扩展, 因此会被提升为0x00000000

if (a == b - 1)
//等价于
if (0x000000ff == 0xffffffff)

因此在有可能出现类型的地方一定要格外小心.

多线程调DLL

最近写代码一不小心又着了多线程的道, 背景如下:
前不久写了这样一个DLL:

const wchar_t *a = L"xxxx";
const wchar_t *b = L"xxxx";

int do_something_a(struct axx *param_a)
{
...
}
int do_something_b(struct bxx *param_b)
{
...
}


在do_something_a与do_something_b中分别用到了字符串a, b.本来这样相安无事, 可是很多地方会用到这个DLL的代码, 但是字符串a, b并不一样, 而字符中a, b可以根据param_a, param_b中的信息来生成, 本着代码正交性的原则, 将DLL重构如下:

wchar_t a[..];
wchar_t b[..];

int do_something_a(struct axx *param_a)
{
gen_a(param_a, a);
...
}
int do_something_b(struct bxx *param_b)
{
gen_b(param_b, b);
...
}

这样咋一看是没什么问题, 代码简洁了, 程序完美了. 可是我忽略了两个问题, 一个进程中不管调LoadLibrary多少次, DLL只会被加载一次. 而我这个DLL是会在多个线程同时加载使用的.
这样一来, 问题就来了, 由于全局数组a与b的存在, 所有的函数都不是线程安全的, 在低并发量的线程中冲突并不严重, 所以问题很难发现, 但是在高并发的线程中两个函数就会大量执行失败.

找到问题了后将代码重构如下:

int do_something_a(struct axx *param_a)
{
wchar_t a[..];
gen_a(param_a, a);
...
}
int do_something_b(struct bxx *param_b)
{
wchar_t b[..];
gen_b(param_b, b);
...
}

这次bug再次给我提了醒, 多线程代码要处处小心, 一不小心就会掉坑里.

由于滥用void *引发的bug

我一向认为在写代码时,void *滥用是有问题的,在最近的一次代码中, 有类似这样一段代码:

 
[cc lang=”C”]int send(void *buff, unsigned long size);

int xx_func(char *buff, unsigned long size)
{
unsigned send_size;
………

send(&buff, send_size);
return 0;
}[/cc]

暂且不论为什么作者会错写成取地址,但其原意是想发送经过处理后的buff里面的内容, 但是编译器是不会报错的,因为void *默认兼容所有类型,如果把代码改成下面这样:

[cc lang=”C”]int send(unsigned char *buff, unsigned long size);

int xx_func(char *buff, unsigned long size)
{
unsigned long send_size;
………

//send(&buff, send_size);
//send(buff, send_size);
send((unsigned char *)buff, send_size);
}[/cc]
其实前两种编译在进行参数检查时,都会报警的,只有写成第三种形式,编译器才会真正通过,如你写成第三种形式时不会去看buff, 到底是什么,那我也没话说了。当然如果你忽略警告我也没啥可说的了哈哈,我只想说应该尽可能的去利用编译器来发现潜在bug.

注:从语义上来说,send(void *buff, unsigned long size)会使人疑惑, 这个size是byte? word? dword?, 如果是unsigned char *,那size当然就是byte, 如果是unsigned short *, 那size当然就是word.(X86 platform)

一次手误引出的bug

  在工作中写了这样一段代码:

 1 struct xx_param {
 2        int index1;
 3        int index2;
 4 };
 5 
 6 //func1, func2, func3为三个函数指针
 7 
 8 int init(a_func_t *func1, b_func_t *func2, c_func_t *func3, void *param)
 9 {
10        struct xx_param *p = (struct xx_param *)param;
11         func1(p->index1, p->index2);
12 }
13 
14 int prepare_init(int xx_index_1, int xx_index_2)
15 {
16         int err;
17         struct xx_param        xx;
18         
19        ......
20 
21         xx.index1 = xx_index1;
22         xx.index2 = xx_index2;
23 
24         if(init(func1, func2, func3, &xx_index1))
25                err = -xxx;
26 20        ......
27 
28         return err;
29 }

  很明显这个错误很普通,就是第24行应该为&xx,但是我手误错写成了&xx_index1。但是编译器不会报警,下面来说一下这个错误的神奇之处,在Debug下永远是正确的,但是Release下永远跑飞。下面来分析一下为什么:

  在Debug模式下参数是由栈来传递的,那么参数xx_index_1, xx_index_2在栈中的布局 与 xx结构体变量在栈中的布局完全一样,因此虽然我取错了地址,但是根据&xx_index_1处得到的地址来获到xx_index1, xx_index_2,其实相当于将结构体变量xx赋值然后从&xx地址处取xx.index1,xx.index2变量(貌似还少了几次内存拷贝,虽然他是一个bug, ^_&),因此在Debug模式下这断代码永远是正确的。

  但是在Release下却不正确,因为在VS下,O2的优化级别下xx_index_1, xx_index_2是由寄存器ecx, edx来传递的,xx_index_1, xx_index_2两个变量的值不会全部在栈中分配(由于对xx_index1进行取地址,因此会导致编译器为xx_index1分配内存,但是xx_index_2确不会分配内存),这样从&xx地址去取数据时xx.index2变量取到的数据总是垃圾数据,因此软件在Release下总是挂掉,但如果将优化关掉却又能良好运行。

  这个Bug很简单,但是现象却很诡异,因此感觉值得一记。

  

 

关于用DLL接中使用std::vector之后出现的问题

最近在代码中用了这样一个DLL,采用静态加载方式使用,原型类似如下:

XXX_API  int xx_func(std::vector<struct xx> &xx_tbl, ..., ...);
//代码中会用xx_tbl.push_back(xx);之类的代码向xx_tbl里面填充数据

但是却出现一个奇葩问题,每当调用这个DLL的程序退出时Debug版本有很大概率会崩溃在这个std::vector<struct xx>的析构函数上。

研究了好久才发现,当DLL中调用push_back函数时,其实std::vector<struct xx>的构造函数分配的内存是属于这个DLL的资源,当程序退出时会首先卸载这个DLL程序,那么与他相关的内存也随之被释放。

当主程序最后退出时,就会引发xx_tbl的析构函数,但是由于xx_tbl中的某些元素的内存是在DLL中分配的,而且已经被释放了,那么这些内存在被析构函数释放时就会引出错误,Debug版的代码是有内存检查的,因此每次Debug代码退出时就会崩溃。

因此,对于DLL中尽量采用纯C的结构,不要使用对象。

————————————————————————————————–
后来找到原因,因为动态链接库使用的CRT库是静态链接的,与Exe程序使用的是两个不同的CRT,因此在DLL中向vector中push数据之后会,会调用DLL链接的CRT进行分配内存,如果在Exe中使用了被DLL操作过的vector变量时,就会导致vector变量进行resize,而由于Exe与DLL使用的是两个CRT库,这时就会造成信息错乱,引起程序崩溃,只要将所有DLL设成使用动态加载CRT即可解决此问题。

类似sprintf这类变参可能出现的bug

  中午吃完饭照例去云风大神的blog上去逛一圈,果然有新发现,如题:

1     char buff[3];
2     char data;
3     sprintf(buff, "%02x", data);

  咋一看,data最大等于0xff应该不会错,可以如果编译器默认char为signed char,而且data = -1,以十六进制看应该为0xff,这么看也没有错。

  关键在于变参,在C语言的变参中,小于int长度的数据压栈时一律扩展为int型, 那么问题来了,符号型数据在进行类型扩展时是会扩展符号的,这么看其实

data = -1;
sprintf(buff, "%02x", data);
//(int)-1的16进制等效于0xffffffff,因此等效于下面这句话
sprintf(buff, "%02x", 0xffffffff);

如此看来,溢出了,这种问题极易出现,而且不易发现。