操作数值超出系统的限制_C lang 补遗(一):不符合直觉的数值问题

这篇文章解释了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
C Type allowed for integer constants
  • 如上表,在C90标准下,32位系统中,十进制常量2147483648会依次匹配,intlong intunsigned 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

这篇文章解释了Introduction to CSAPP(二)信息的表示与处理·概述 - Yannick的文章 - 知乎中,引子提出来的两个问题。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值