标签:c/c++
运算符两边操作数的类型转化
此时,操作数类型的转换规则如下:
图中的横向箭头:针对运算符两边是同类型的数据(不同级别的类型当然也适用!)
表示必须的转换(虽然运算符两边的操作数类型相同),如两个float型数参加运算,虽然它们类型相同,但仍要先转成double型再进行运算,结果亦为double型。两个char类型的数据进行相关的操作也是一样(这也是汇编代码中为什么将char类型的数据放入eax这样的32位的寄存器中再进行运算的原因了)。
图中的纵向箭头:针对运算符两边是不同类型的数据
举一个例子:如一个long型数据与一个int型数据一起运算,需要先将int型数据转换为long型(就算两者所占的空间大小是一样的), 然后两者再进行运算,结果为long型。
【注意】所有这些转换都是由系统自动进行的, 使用时最重要的是结果的类型。这些转换可以说是自动的,当然,c语言也提供了以显式的形式强制转换类型的机制。
赋值情况下数据类型转换过程中的数值变化
较低类型的数据转换为较高类型
一般只是形式上有所改变,而不影响数据的实质内容。(除了有符号数转换为无符号数且转换前后类型空间大小一致这样的情况)
较高类型的数据转换为较低类型
赋值中的类型转换
当赋值运算符两边的运算对象类型不同时,将要发生类型转换,转换的规则是:把赋值运算符右侧表达式的类型转换为左侧变量的类型。
(1)浮点数与整形
将浮点数(单双精度)转换为整数时,一般是舍弃浮点数的小数部分, 只保留整数部分(整数部分可以直接放入对应的空间)。
将整型值赋给浮点型变量,数值不变,只将形式改为浮点形式, 即小数点后带若干个0.
【注意】赋值时的类型转换实际上是强制的。
下面是浮点数的便是范围:
【规范型】
【非规范型】
(2)单、双精度浮点型
由于c语言中的浮点值总是用双精度表示的,所以float型数据(转换为double类型)只是在尾部加0延长为double型数据参加运算,然后直接赋值。double型数据转换为float型时,通过截尾数来实现,截断前要进行四舍五入操作。
(3)char型与int型
int型数值赋给char型变量时,只保留其最低8位,高位部分舍弃。
char型数值赋给int型变量时,更多的是一些编译程序在转换时,若char型数据值大于127,就作为负数处理。即如果原来char型值可正可负,则转换后也仍然保持原值,只是数据的内部表示形式有所不同。另外,还有一些编译器不管其值大小都作正数处理(少见)。
(4)int型与1ong型
如果int是16bit大小的话,转换过程同char类型和int类型。如果不是,就只是纯粹的类型改变。
(5)无符号类型的数据
计算机中数据的存储形式
计算机中数据用补码表示,int型量最高位是符号位,为1时表示负值,为0时表示正值。
其他形式的数据类型转换
输出时候的数据类型转换过程:
printf操作
char a = 23;
printf("%d", a);
上述代码存在数据类型的转换过程,因为 %d
是针对int类型的数据输出的。见下面的C代码和汇编:
- 有符号数的输出
```
signed char x = 0x7f;
printf("%d\n", ++x);
// 对应的汇编代码
00853CFB mov al,byte ptr [x]
00853CFE add al,1
00853D00 mov byte ptr [x],al
00853D03 movsx ecx,byte ptr [x] // 对于有符号数就使用movsx符号位扩展的寄存器赋值
00853D07 mov esi,esp
00853D09 push ecx
```
- 无符号数的输出
```
unsigned char h = 21;
printf("%d\n", ++h);
// 对应的汇编代码
00853D23 mov al,byte ptr [h]
00853D26 add al,1
00853D28 mov byte ptr [h],al
00853D2B movzx ecx,byte ptr [h] // 对于无符号数就使用movzx的0扩展的寄存器赋值
00853D2F mov esi,esp
00853D31 push ecx
```
memcpy(mybuf, buf, len)
【最后·注意】
1. 因为不管表达式的值怎样,系统都自动将其转为赋值运算符左部变量的类型。
2. c语言最初是为了替代汇编语言而设计的,所以类型变换比较随意。当然,用强制类型转换是一个好习惯,这样,至少从程序上可以看出想干什么。
3. C语言的 显式/隐式 类型转换,都有一个中间变量的存在,原数据的类型、内容都不变。
一般情况下还是避免产生溢出的现象。
该部分来自【推荐】:http://coolshell.cn/articles/11466.html
溢出类型
unsigned整型溢出
对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。例如:
unsigned char x = 0xff;
printf("%d\n", ++x);
上面的代码会输出:0 (因为0xff + 1是256,与2^8求模后就是0),在类型变换之前,x的值就已经变成了0。
signed整型溢出
对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。
signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);
上面的代码会输出:-128,因为0x7f + 0x01得到0x80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。
溢出带来的问题
整形溢出导致死循环
#define MAX_LEN 32767
... ...
short len = 0;
while (len < MAX_LEN) {
len += 2;
}
上面这段代码可能是很很多都喜欢写的代码,其中的MAX_LEN 可能会是个比较大的整型,比如32767,我们知道short是16bits,取值范围是-32768 到 32767 之间。但是,上面的while循环代码就会造成整型溢出,因为len是一个有符号的整型,操作过程中每次增加2,最终会成负数,从而导致不断的死循环。
所以,类似 MAX_LEN
的宏,比len大出一个数量级是最好的。
对于上述问题,引出一个小问题:
<
符号两边的操作数可能存在类型转换的问题,看上述代码对应的汇编代码:
// while (len < MAX_LEN)
01033D78 movsx eax,word ptr [len]
01033D7C cmp eax,7FFFh
首先, MAX_LEN
宏常量在编译的过程中是没有类型的检查的,只是单纯的替换,我们可以理解为常量,但是这里的数据类型转换和宏是没有关系的,上面已经介绍了,低于int类型的short或者char的操作数出现在运算符两边都要进行类型转换,即转换为int类型的数据。可以看出,len通过符号位扩展从而成为int类型的数据。
下面有一个类似的例子:
char s = 128;
int t = 1;
if (s > t)
{
printf("ai");
}
// if (s > t) 对应的汇编代码
if (s > t)
01033D52 movsx eax,byte ptr [s] // 类型转换,符号位扩展(因为是有符号数)
01033D56 cmp eax,dword ptr [t] // 表示取双字(因为是int类型的数据)
整形转型时的溢出(类型转换带来的溢出问题)
int copy_something(char *buf, int len)
{
#define MAX_LEN 256
char mybuf[MAX_LEN];
... ...
if(len > MAX_LEN){ // [1]
return -1;
}
return memcpy(mybuf, buf, len);
}
上面这个例子中,还是[1]处的if语句,看上去没有问题,但是len是个signed int,而memcpy则需一个size_t(size_t一般就是unsigned int/long int)的len,也就是一个unsigned 类型。于是,len会被提升为unsigned,此时,如果我们给len传一个负数的实参,会通过if的检查,但在memcpy里会被提升为一个正数,于是我们的mybuf就是overflow了。这个会导致mybuf缓冲区后面的数据被重写。
分配内存过程中出现的溢出
nresp = packet_get_int();
if (nresp > 0) {
response = xmalloc(nresp*sizeof(char*));
for (i = 0; i < nresp; i++)
response[i] = packet_get_string(NULL);
}
上面这个代码中,nresp是size_t类型,这个示例是一个解数据包的示例,一般来说,数据包中都会有一个len,然后后面是data。如果我们精心准备一个len(对应代码中的nresp),比如:1073741825(在32位系统上,指针占4个字节,unsigned int的最大值是0xffffffff,我们只要提供0xffffffff/4 的值——0x40000000,这里我们设置了0x4000000 + 1), nresp就会读到这个值,然后nresp*sizeof(char*)就成了 1073741825 * 4,于是溢出,结果成为了 0x100000004,然后求模,得到4。于是,malloc(4),于是后面的for循环1073741825次,就可以干坏事了(经过0x40000001的循环,用户的数据早已覆盖了xmalloc原先分配的4字节的空间以及后面的数据,包括程序代码,函数指针,于是就可以改写程序逻辑。
其实,就是因为xmalloc函数入参类型转换带来的溢出,跟上个例子如出一辙。
缓冲区溢出导致安全问题
int func(char *buf1, unsigned int len1,
char *buf2, unsigned int len2 )
{
char mybuf[256];
if((len1 + len2) > 256){ //<--- [1]
return -1;
}
memcpy(mybuf, buf1, len1);
memcpy(mybuf + len1, buf2, len2);
do_some_stuff(mybuf);
return 0;
}
上面这个例子本来是想把buf1和buf2的内容copy到mybuf里,其中怕len1 + len2超过256 还做了判断,但是,有两种情况可能会引起错误:
size_t 的溢出
for (uint i= strlen(s)-1; i>=0; i--) { ... }
for (uint i=v.size()-1; i>=0; i--) { ... }
上面这两个示例是我们经常用的从尾部遍历一个数组的for循环。第一个是字符串,第二个是C++中的vector容器。strlen()和vector::size()返回的都是 size_t,size_t在32位系统下就是一个unsigned int。你想想,如果strlen(s)和v.size() 都是0呢?这个循环会成为个什么情况?于是strlen(s) – 1 和 v.size() – 1 都不会成为 -1,而是成为了 (unsigned int)(-1),一个正的最大数。导致你的程序越界访问。
**【注意】**类型提升是有顺序的,如果代码是 int a; int i= strlen(s)-a
,则有:
先是a的类型提升为size_t类型的中间变量,运算 strlen(s)-a
,之后将结果转换为int类型的中间变量赋值给i。
编译器的优化
int len;
char* data;
if (data + len < data){ // 防止len非法的判断,可能会在-O2下被优化
printf("invalid len\n");
exit(-1);
}
C99标准中明文规定:定义了 指针+/-一个整型
的行为,如果越界了,则行为是undefined,也就是说这事交给编译器实现了,编译器想咋干咋干,那怕你想把其优化掉也可以。这种情况下,如果我们设置-O2的编译选项,就可能会被优化。
正确检测整型溢出
在看过编译器的这些行为后,你应该会明白——“在整型溢出之前,一定要做检查,不然,就太晚了!”。看下面的代码:
void foo(int m, int n)
{
size_t s = m + n;
.......
}
上面这段代码有两个风险:
1)有符号转无符号
2)整型溢出。
这两个情况在前面的那些示例中你都应该看到了。所以,你千万不要把任何检查的代码写在 s = m + n 这条语名后面,不然就太晚了。
【注意(上面有说)】有些同学也许会以为size_t是无符号的,而根据优先级 m 和 n 会被提升到unsigned int。其实不是这样的,m 和 n 还是signed int,m + n 的结果也是signed int,最后才会把这个结果转成unsigned int 赋值给s。
一个变形
void foo(int m, int n)
{
size_t s = m + n;
if ( m>0 && n>0 && (SIZE_MAX - m < n) ){
//error handling...
}
}
【注意】 (SIZE_MAX – m < n)
这个判断,为什么不用m + n > SIZE_MAX呢?因为,如果 m + n 溢出后,就被截断了,所以表达式恒真,也就检测不出来了。另外,(SIZE_MAX – m < n)
表达式中,m和n分别会被提升为unsigned(见上述类型变换的顺序)。
【BUT】但是上面的代码是错的,因为:
1)检查的太晚了,if之前编译器的undefined行为就已经出来了(你不知道什么会发生)。
2)就像前面说的一样,(SIZE_MAX – m < n) 可能会被编译器优化掉。
3)另外,SIZE_MAX是size_t的最大值,size_t在64位系统下是64位的,严谨点应该用INT_MAX或是UINT_MAX
【SO】正确的代码应该是下面这样:
void foo(int m, int n)
{
size_t s = 0;
if ( m>0 && n>0 && ( UINT_MAX - m < n ) ){
//error handling...
return;
}
s = (size_t)m + (size_t)n; //
}
将s的赋值放在后面执行
改变s赋值的操作中m和n类型变换的顺序(原因是,我们总是不希望溢出情况的发生,先转换类型可以在一定程度上避免溢出的发生。)
类似的例子
if (n > 0 && m > 0 && SIZE_MAX/n >= m ){
size_t bytes = n * m;
...
}
如果n和m都是signed int,那么这段代码是错的。正确的应该像上面的那个例子一样,至少要在nm时要把 n 和 m 给 cast 成 size_t。因为,nm可能已经溢出了,已经undefined了,undefined的代码转成size_t已经没什么意义了。上面的代码仅在m和n是size_t的时候才有效。
二分取中搜索算法中的溢出
该部分在《编程之美》中也有说明。
int binary_search(int a[], int len, int key)
{
int low = 0;
int high = len - 1;
while ( low<=high ) {
int mid = (low + high)/2;
if (a[mid] == key) {
return mid;
}
if (key < a[mid]) {
high = mid - 1;
}else{
low = mid + 1;
}
}
return -1;
}
上面这个代码中,你可能会有这样的想法:
1) 我们应该用size_t来做len, low, high, mid这些变量的类型。没错,应该是这样的,比如第四行 int high = len -1;
如果len为0,那么就“high大发了”。
2) 无论你用不用size_t。我们在计算mid = (low+high)/2; 的时候,(low + high) 都可能溢出。正确的写法应该是:
int mid = low + (high - low)/2;
所以溢出有时候不仅仅可以通过数据类型避免,也和我们代码写法相关。
通过一个例子说明:
unsigned char a, b;
a = b = 250;
unsigned char c = (a + b) >> 1;
如果查看汇编代码的话,可以看到:
008F13E8 movzx eax,byte ptr [a]
008F13EC movzx ecx,byte ptr [b]
008F13F0 add eax,ecx
008F13F2 sar eax,1
008F13F4 mov byte ptr [c],al
说明汇编处理的时候也是转为32位寄存器进行处理的,而不使用的8位寄存器(char类型的数据)进行处理(16位的寄存器:AH&AL=AX,其中AH和AL是8位寄存器)。所以,一般不会出现饱和处理的情况。但这又是为什么呢?答案就是类型转换, +
两边的数据会进行强制类型转换,变换为int类型的数据习性相关操作,操作完成之后,根据dst的数据类型截取相应的位数空间即可。
所以,此处的溢出不是发生在 +
处,而是发生在对c赋值的时候。