这篇文章解释了Introduction to CSAPP(二)信息的表示与处理·概述 - Yannick的文章 - 知乎中,引子提出来的两个问题。
有几个C语言程序的例子:
例一
#include <stdio.h>
int main(){
if (-2147483648 < 2147483647){
printf("literal value test passedn");
}
int i = -2147483648;
if (i < 2147483647){
printf("varible value test passedn");
}
if (-2147483647 - 1 < 2147483647){
printf("calculated value test passedn");
}
return 0;
}
实验操作:
# 需要安装docker
$ docker run -t -i --rm i686/ubuntu bash
# 拉取完镜像后,发现prefix变化,说明进入了docker镜像
root@d990544488db:/$ cd ~ # 进入用户目录
root@d990544488db:~$ apt-get update && apt-get install gcc libc6-dev binutils
root@d990544488db:~$ vi compare.c # 创建并编辑文件,把上述代码复制
root@d990544488db:~$ gcc -o compare compare.c
root@d990544488db:~$ ./compare
# 以下是输出
varible value test passed
calculated value test passed
我们发现,在ISO C90标准下,在32位系统上,上面的的C程序结果与我们预期的事实并不相符。要理解为什么会出现这种情况,我们需要知道:
- 编译器如何处理字面量
- 高级语言中运算规则
- 高级语言与指令之间的对应
- 机器指令的执行过程
- 机器级数据的表示和运算
例二
另外一个例子。
#include <stdio.h>
/*
当参数len为0时,返回值应该是0,但是在机器上执行时,却发生访存异常。
但当len为int型时则正常。Why?访问违例地址为何是0xC0000005?
*/
int sum(int a[], unsigned int len){
int i, sum = 0;
for(i = 0; i <= len - 1; i++)
sum += a[i];
return sum;
}
int main(){
int a[5] = {1, 2, 3, 4, 5};
printf("result is %d", sum(a, 0));
return 0;
}
实验操作
$ gcc -g -o out compare.c
# 或者gdb ./out
$ lldb ./out
(lldb) run
# 以下是输出
Process 18712 launched: '/Users/yansong/C_experiments/out' (x86_64)
Process 18712 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x7ffeefc00000)
frame #0: 0x0000000100000ec2 out`sum(a=0x00007ffeefbff080, len=0) at compare.c:6:12
3 int sum(int a[], unsigned int len){
4 int i, sum = 0;
5 for(i = 0; i <= len - 1; i++)
-> 6 sum += a[i];
7 return sum;
8 }
9
Target 0: (out) stopped.
参数为0时,应该返回0,但是报了segmentation fault
,具体错误时EXC_BAD_ACCESS
,请思考为什么是“错误的访问”这个错误。
理解该问题需要知道:
- 高级语言中运算规则
- 机器指令的含义和执行
- 计算机内部的运算电路
- 异常的检测和处理
- 虚拟地址空间
Solution 1
我们实验发现
// 这一句的逻辑判断是假值
if (-2147483648 < 2147483647){
printf("literal value test passedn");
}
对于另外两种情况成立的原因:
- 在C90标准下,32位系统中,编译器遵循ILP32的数据模型
- 下表 32 位和 64 位数据模型 加粗的为不同模型下表现一致的。
- 对于常量,编译器判定常量类型时,会依次尝试可能的情况。需要注意的是,C在进行常量匹配的时候,是不考虑负数的。也就是说不会有负的常量。负数是在有了正的常量之后,对正常量进行一次一元运算法进行一次隐式转换达成的。
![b5cf948ed52c0fb6672457a71421f732.png](https://img-blog.csdnimg.cn/img_convert/b5cf948ed52c0fb6672457a71421f732.png)
- 如上表,在C90标准下,32位系统中,十进制常量2147483648会依次匹配,
int
,long int
,unsigned long int
(负号单独处理),2147483648在前两个类型无法匹配时,匹配到了unsigned long int
,来存储自己的值。随后进行了一次minus的单目运算符运算,所以这个常量最终以unsigned long int
的类型存储了-2147483648的值。 - 在编译的时候会遇到
compare.c:4:3: warning: this decimal constant is unsigned only in ISO C90 [enabled by default]
这样的warning。 - 此时
-2147483648 < 2147483647
就变成了无符号与带符号数的比较,所以两遍都会被看作无符号数进行比较,所以这个表达式会表示假值。 - 只能在32位机器上的原因是,64位机器
long int
一般有64位可以匹配到-2147483648并合理地表示这个值。
对于另外两种情况成立的原因:
int i = -2147483648; // i 十分确定是 signed int
if (i < 2147483647){// 因此这里比较是带符号数的比较
printf("varible value test passedn");
}
// 这里-2147483647是带符号数,运算之后仍然是带符号数
if (-2147483647 - 1 < 2147483647){
// 因此这里的比较还是带符号数的比较
printf("calculated value test passedn");
}
Solution 2
原因:
#include <stdio.h>
/*
当参数len为0时,返回值应该是0,但是在机器上执行时,却发生访存异常。
但当len为int型时则正常。Why?访问违例地址为何是0xC0000005?
*/
// 在windows下违例的地址是0xC0000005,违例原因是access violation
int sum(int a[], unsigned int len){
int i, sum = 0;
// 2. 实际原因在于len是unsigned int,len本身是0,减一后发生了溢出
// 3. 在这个判断比较中,两遍都以无符号的方式进行比较,len - 1被视作4294967295
// 4. 所以可以进入循环
for(i = 0; i <= len - 1; i++)
// 1. 违例表现是access violation的原因是,这里的指针访问超出了数组的边界,即i超出了a数组的可访问范围
// 5. 违例原因是,在循环过程中,指针访问到了其他程序受保护的内存
sum += a[i];
return sum;
}
int main(){
int a[5] = {1, 2, 3, 4, 5};
printf("result is %d", sum(a, 0));
return 0;
}
下面是知识点回顾
C 整型比较时的隐式转换
对于C语言来说,C语言标准没有指定有符号数在内存里要采取哪种表示方式,但是基本上所有的机器都采用补码表示。对于无符号数,无论用原码、补码还是反码,它都是一致的。
对于Java语言来说,整数数据类型的表示是补码,注意Java是没有符号概念的,reference。
在 C 语言中,如果不加关键字限制,默认的整型是有符号的。如果想要无符号数的话,需要在数字后面加 U
或者u
,例如下面的代码段
int a_signed_number = -15213;
unsigned int a_unsigned_number = 15213U;
在进行有符号和无符号数的互相转换时:
- 具体每一个字节的内存表示值不会改变,改变的是计算机解释当前值的方式
- 如果一个表达式既包含有符号数也包含无符号数,那么会被隐式转换成无符号数进行比较
C 整型的计算溢出
- 无符号数加法
如果两个w
位的数字相加,结果是w+1
位的话,那么就会丢弃掉最高位,实际上是做了一个 mod 操作(公式为 ) - 有符号数加法
操作过程和无符号加法一样,只是解释的时候会有不同,因此会得到正溢出(positive overflow)和负溢出(negative overflow)两种。- 正溢出就是数值太大把原来为 0 的符号位修改成了 1,反而成了负数;
- 负溢出是数值太小,把原来为 1 的符号位修改成了 0,反而成了正数。
C Data model
C 语言整型常量匹配规则
- 依据一定的顺序
- 没有负整型:先匹配正整型,后进行minus单目运算符运算,进行隐式转换
![b5cf948ed52c0fb6672457a71421f732.png](https://img-blog.csdnimg.cn/img_convert/b5cf948ed52c0fb6672457a71421f732.png)
这篇文章解释了Introduction to CSAPP(二)信息的表示与处理·概述 - Yannick的文章 - 知乎中,引子提出来的两个问题。