4. 乘除运算及浮点数运算

4.1 整数乘法运算

1. 整数乘法

通常,高级语言中两个n位整数相乘得到的结果通常也是一个n位整数,即结果只取2n位乘积中的低n位。
–例如,在C语言中,参加运算的两个操作数的类型和结 果的类型必须一致,如果不一致则会先转换为一致的 数据类型再进行计算。

Q: 在计算机内部,一定有 x 2 ≥ 0 x^2≥0 x20 吗?
A:

 若x是带符号整数,则不一定!
 如x是浮点数,则一定!


例如,当n=4 时, 52=-7<0

     0101
x    0101
——————————
     0101           结果溢出
+  0101       只取低4位,值为-111B=-7
——————————
 00011001    

Q: 如何判断返回的z是正确值?

int mul(intx, inty)
{
	 int z=x*y; 
	 return z;
} 

A:
 ①使用程序语言来判断: ! x ∣ ∣ z / x = = y ! x || z/x==y !xz/x==y为真(除数为0/反向验算)
 ②编译器(使用指令)进行判断:
  当 − 2 n − 1 ≤ x ∗ y < 2 n − 1 -2n-1≤ x*y < 2n-1 2n1xy<2n1(不溢出)时
  即:乘积的高n位为全0或全1,并等于低n位的最高位
  即:乘积的高n+1位为全0或全1

若x、y和z都改成unsigned类型,则编译器判断方式为:乘积的高n位为全0,则不溢出

2. 溢出判断

假定:

n位无符号整数 x u x_u xu y u y_u yu p u = x u ∗ y u p_u=x_u * y_u pu=xuyu
对应机器数 X u X_u Xu Y u Y_u Yu P u P_u Pu
n位带符号整数 x s x_s xs y s y_s ys p s = x s ∗ y s p_s=x_s * y_s ps=xsys
对应机器数 X s X_s Xs Y s Y_s Ys P s P_s Ps

X u = X s Xu=Xs Xu=Xs Y u = Y s Yu=Ys Yu=Ys,则 P u = P s Pu=Ps Pu=Ps,但 P u h ≠ P s h Puh≠ Psh Puh̸=Psh(即高位不一定相等)
可用无符号乘来实现带符号乘,但高n位无法得到,故不能判断溢出。

  • X*Y的高n位可以用来判断溢出,规则如下:
    – 无符号:若高n位全0,则不溢出,否则溢出
    – 带符号:若高n位全0或全1且等于低n位的最高位,则不溢出。
    4. 乘除运算及浮点数运算_第1张图片
  • 硬件不判溢出,仅保留2n位乘积,供软件使用
  • 如果程序不采用防止溢出的措施,且编译器也不生成用于溢出处理的代码,就会发生一些由于整数溢出而带来的问题。
  • 指令:分无符号数乘指令带符号整数乘指令
  • 乘法指令的操作数长度为n,而乘积长度为2n,乘法指令不生成溢出标志,编译器可使用2n位乘积来判断是否溢出

以下程序存在什么漏洞,引起该漏洞的原因是什么。

/* 复制数组到堆中,count为数组元素个数*/ 
int copy_array(int *array, int count) 
{ 
   int i;  
   /* 在堆区申请一块内存*/
   int *myarray = (int *) malloc(count*sizeof(int));
   if (myarray == NULL)
   	 return -1; 
   for (i = 0; i < count; i++)
   	 myarray[i] = array[i]; 
   return count; 
/*数溢出,以一段预设信息覆盖一个已分配的堆缓冲区,
造成远程服务器崩溃或者改变内存数据并执行任意代码*/

c o u n t ∗ s i z e o f ( i n t ) count*sizeof(int) countsizeof(int) 会溢出。
c o u n t = 2 30 + 1 count=2^{30}+1 count=230+1时, c o u n t ∗ s i z e o f ( i n t ) = 4 count*sizeof(int)=4 countsizeof(int)=4。不断向内存中复制数据,会使堆(heap)中大量数据被破坏!

3. 变量与常数之间的乘运算

  整数乘法运算比移位和加法等运算所用时间长,通常一次 乘法运算需要多个时钟周期,而一次移位、加法和减法等运算只要一个或更少的时钟周期,因此,编译器在处理变量与常数相乘时,往往以移位、加法和减法的组合运算来代替乘法运算
  例如,对于表达式 x ∗ 20 x*20 x20,编译器可以利用 20 = 16 + 4 = 24 + 22 20=16+4=24+22 20=16+4=24+22,将 x ∗ 20 x*20 x20 转换为 ( x < < 4 ) + ( x < < 2 ) (x<<4)+(x<<2) (x<<4)+(x<<2),这样,一次乘法转换成了两次移位和一次加法。

4.2 整数除法运算

1. 整数除法

Q: 对于带符号整数来说,n位整数除以n位整数,除 − 2 n − 1 / − 1 = 2 n − 1 -2n-1/-1= 2n-1 2n1/1=2n1 会发生溢出外,其余情况都不会发生溢出。Why?
A: 因为商的绝对值不可能比被除数的绝对值更大,因而不会发生溢出, 也就不会像整数乘法运算那样发生整数溢出漏洞。

  • 因为整数除法,其商也是整数,所以,在不能整除时需要进行舍入, 通常按照朝0方向舍入,即正数商取比自身小的最接近整数(Floor, 地板),负数商取比自身大的最接近整数(Ceiling,天板)。 例如:7/2=3, -7/2=-3。

  • 整除0的结果无法用一个机器数表示。整数除法时,除数不能为0,否则会发生“异常”,此时,需要调出操作系统中的异常处理程序来处理。

  • 两个代码段

	//代码段一: 
	int a = 0x80000000;
	int b = a / -1; 
	printf("%d\n", b);
	
	//运行结果为-2147483648 
	//除以-1 被优化成取负指令neg, 故未发生除法溢出
	
	//代码段二: 
	int a = 0x80000000; 
	int b = -1; 
	int c = a / b; 
	printf("%d\n", c); 
	
	//运行结果为“Floating point exception”
	//CPU检测到了异常

2.变量与常数之间的除运算

对于整数除法运算,由于计算机中除法运算比较复杂,而且不能用流水线方式实现,所以一次除法运算大致需要30个或更多个时钟周期,比乘法指令的时间还要长!

  • 为了缩短除法运算的时间,编译器在处理一个变量与一个2的幂次形式的整数相除时,常采用右移运算来实现。
     –无符号:逻辑右移
     –带符号:算术右移
  • 结果一定取整数
    ①能整除时,直接右移得到结果,移出的为全0
     例如, 12 / 4 = 3 : 00001100 > > 2 = 00000011 12/4=3:0000 1100>>2=0000 0011 12/4=300001100>>2=00000011
         − 12 / 4 = − 3 : 11110100 > > 2 = 11111101 -12/4=-3:1111 0100 >>2=1111 1101 12/4=311110100>>2=11111101
    ②不能整除时,采用朝零舍入,即截断方式
      –无符号数、带符号正整数(地板): 移出的低位直接丢弃
      –带符号负整数(天板):加偏移量(2 k -1),然后再右移 k 位,低位截断(这里 K 是右移位数)

例如:
无符号数
14 / 4 = 3 14/4=3 14/4=3 00001110 > > 2 = 00000011 0000 1110>>2=0000 0011 00001110>>2=00000011
带符号负整数 − 14 / 4 = − 3 -14/4=-3 14/4=3
若直接截断,则 11110010 > > 2 = 11111100 = − 4 ≠ − 3 1111 0010 >>2=1111 1100=-4≠-3 11110010>>2=11111100=4̸=3
应先纠偏,再右移: k = 2 k=2 k=2, 故 $(-14+22-1)/4=-3 $
即: 11110010 + 00000011 = 1111010111110101 > > 2 = 11111101 = − 3 1111 0010+0000 0011=1111 0101 1111 0101>>2=1111 1101=-3 11110010+00000011=1111010111110101>>2=11111101=3

Q: 假设 x 为一个int型变量,请给出一个用来计算 x /32的值的函数div32。 要求不能使用除法、乘法、模运算、比较运算、循环语句和条件语句, 可以使用右移、加法以及任何按位运算。

A:
x x x为正数,则将 x x x右移 k k k位得到商;
x x x为负数,则 x x x需要加一个偏移量 ( 2 k − 1 ) (2k-1) (2k1)后再右移 k k k位得到商。
因为 32 = 25 32=25 32=25,所以 k = 5 k=5 k=5。 即结果为: ( x > = 0 ? x : ( x + 31 ) ) > > 5 ( x>=0 ? x : (x+31))>>5 (x>=0?x:(x+31))>>5
但题目要求不能用比较和条件语句,因此要找一个计算偏移量 b b b的方式
这里, x x x为正时 b = 0 b=0 b=0 x x x为负时 b = 31 b=31 b=31. 因此,可以从 x x x的符号得到 b b b. x > > 31 x>>31 x>>31 得到的是32位符号,取出最低5位,就是偏移量 b b b

int div32(int x) 
{  
/* 根据x的符号得到偏移量b */
	int b=(x>>31) & 0x1F; 
	return (x+b)>>5; 
}

4.3 浮点数运算

1. 浮点数运算及结果

(1)浮点数运算异常

设两个规格化浮点数分别为 A = M a ∗ 2 E a A=Ma *2^{Ea} A=Ma2Ea , B = M b ∗ 2 E b B=Mb*2^{Eb} B=Mb2Eb ,
A + B = ( M a + M b ∗ 2 − ( E a − E b ) ) ∗ 2 E a A+B=(Ma +Mb*2^{-(Ea-Eb)})* 2^{Ea } A+B=(Ma+Mb2(EaEb))2Ea (假设Ea>=Eb)
A − B = ( M a − M b ∗ 2 − ( E a − E b ) ) ∗ 2 E a A-B=(Ma - Mb*2^{-(Ea-Eb)})* 2^{Ea } AB=(MaMb2(EaEb))2Ea (假设Ea>=Eb)
A ∗ B = ( M a ∗ M b ) ∗ 2 E a + E b A*B=(Ma *Mb)*2^{Ea+Eb } AB=(MaMb)2Ea+Eb
A / B = ( M a / M b ) ∗ 2 E a − E b A/B=(Ma /Mb)*2^{Ea-Eb} A/B=(Ma/Mb)2EaEb
上述运算结果可能出现以下几种情况:

阶码上溢 一个正指数超过了最大允许值 +∞/-∞/溢出
阶码下溢 一个负指数超过了最小允许值 +0/-0
尾数溢出
(尾数溢出,结果不一定溢出)
最高有效位有进位 右规
非规格化尾数 数值部分高位为0
右规或对阶时,右段有效位丢失
左规
尾数舍入

2. IEEE 754 标准规定的五种异常情况

无效运算(无意义)
 – 运算时有一个数是非有限数,如: 加/ 减∞、0 x ∞、∞/∞等
 – 结果无效,如: 源操作数是NaN、0/0、x REM 0、∞REM y 等
除以0(即:无穷大)
数太大(阶上溢): 对于SP,指阶码E >1111 1110 (指数大于127)
数太小(阶下溢): 对于SP,指阶码E < 0000 0001(指数小于-126)
结果不精确(舍入时引起),例如1/3、1/10等不能精确表示成浮点数

上述情况硬件可以捕捉到,因此这些异常可设定让硬件处理,也 可设定让软件处理。让硬件处理时,称为硬件陷阱

3.浮点数除0的问题

	#incldue 
	#incldue 
	
	int mian()
	{
		int a = 1,b = 0;
		printf("Divisionby zero:%d\n",a/b);
		getchar();
		return 0;
	}
	//运行结果异常
	
	int main()
	{
		double x = 1,y = -1.0,z = 0.0;
		printf(“Division by zero:%f   %f\n",x/z,y/z);
		getchar();
		return 0;
	}
	//运行结果:1.#INFOO , -1.#INFOO
	//浮点运算中,一个有限数除以0, 结果为正无穷大(负无穷大)

4. 浮点数加/减运

(1)浮点数加减法基本要点

4. 乘除运算及浮点数运算_第2张图片

(2)IEEE 754的舍入方式的说明

强迫结果为偶数:奇数+1,偶数舍去。最低位为0则舍掉多余位,最低位为1则进位1、使得最低位仍为0(偶数)。

4. 乘除运算及浮点数运算_第3张图片

(3)Extra Bits(附加位)

4. 乘除运算及浮点数运算_第4张图片

(4)Rounding Digits(舍入位)

4. 乘除运算及浮点数运算_第5张图片

(5)运算举例

4. 乘除运算及浮点数运算_第6张图片

(6)舍入举例

	#include  
	main() 
	{ 
		float a; 
		double b;
		a = 123456.789e4; 
		b = 123456.789e4; 
		printf(%f/n%f/n”,a,b);
	} 
	
	/*运行结果如下: 
		1234567936.000000 
		1234567890.000000 
	*/

float可精确表示7个十进制有效数位,后面的数位是舍入后的结果, 舍入后的值可能会更大, 也可能更小.

5. C语言中的浮点数类型

  • C语言中有float和double类型,分别对应IEEE 754单精度浮点数格式和双精度浮点数格式 。
  • long double类型的长度和格式随编译器和处理器类型的不同而有所不同,IA-32中是80位扩展精度格式。

(1)各数据类型的有效位数

int float double
1S + 31位 1S + 8E + (23+1)位 1S + 11E + (52+1)位

(2)各类型转换情况

转换 结果
int --> float 不会发生溢出,但可能有数据被舍入
int / float --> double 因为double的有效位数更多, 故能保留精确值
double --> float / int 可能发生溢出,此外,由于有效位数变少,故可能被舍入
float / double --> int 因为int没有小数部分,所以数据可能会向0方向被截断

(数大——溢出,尾数长——舍入)

(3)浮点数比较运算举例

int x ; float f ; double d ;

//判断是否衡正确
x == (int)(float) x		//false  舍入
x == (int)(double) x		//true
f == (float)(double) f		//true
d == (float) d			//flase	溢出/舍入
f == -(-f);			//true
2/3 == 2/3.0			//false	整数相除-->整数/浮点数相除-->浮点数
d < 0.0    ((d*2) < 0.0)	//true
d > f      -f > -d		//true
d * d >= 0.0			//true
x*x>=0				//false	高n位舍入,低n位首位可能为1
(d+f)-d == f			//false	d很大,f很小,f对阶至高位全为0,则左=0,右=f

浮点数不适用加法结合律
x = – 1.5 x 1038 , y = 1.5 x 1038 , z = 1.0 ( x + y ) + z = ( – 1.5 x 1038 + 1.5 x 1038 ) + 1.0 = 1.0 x + ( y + z ) = – 1.5 x 1038 + ( 1.5 x 1038 + 1.0 ) = 0.0 x = –1.5 x 1038, y = 1.5 x 1038, z = 1.0\\ (x+y)+z = (–1.5x1038+1.5x1038 )+1.0 = 1.0 \\ x+(y+z) = –1.5x1038+ (1.5x1038+1.0) = 0.0 x=1.5x1038,y=1.5x1038,z=1.0(x+y)+z=(1.5x1038+1.5x1038)+1.0=1.0x+(y+z)=1.5x1038+(1.5x1038+1.0)=0.0

6. 浮点数运用举例

(1)Ariana火箭爆炸

  1996年6月4日,Ariana 5火箭初次航行,在发射仅仅37秒钟后 ,偏离了飞行路线,然后解体爆炸,火箭上载有价值5亿美元的 通信卫星。
  原因是在将一个64位浮点数转换为16位带符号整数时,产生了溢 出异常。溢出的值是火箭的水平速率,这比原来的Ariana 4火箭 所能达到的速率高出了5倍。在设计Ariana 4火箭软件时,设计 者确认水平速率决不会超出一个16位的整数,但在设计Ariana 5 时,他们没有重新检查这部分,而是直接使用了原来的设计。

(2)爱国者导弹定位错误

1991年2月25日,海湾战争中,美国在沙特阿拉伯达摩地区设置的爱国者 导弹拦截伊拉克的飞毛腿导弹失败,致使飞毛腿导弹击中了一个美军军营, 杀死了美军28名士兵。其原因是由于爱国者导弹系统时钟内的一个软件错 误造成的,引起这个软件错误的原因是浮点数的精度问题。

4. 乘除运算及浮点数运算_第7张图片
4. 乘除运算及浮点数运算_第8张图片
4. 乘除运算及浮点数运算_第9张图片

从上述结果可以看出:
  用32位定点小数表示0.1,比采用float精度高64倍
   用float表示在计算速度上更慢,必须先把计数值转换为 IEEE 754格式浮点数,然后再对两个IEEE 754格式的数相乘,故采用float比直接将两个二进制数相乘要慢

不能遇到小数就用浮点数表示,有些情况下(如需要将一个整 数变量乘以一个确定的小数常量),可先用一个确定的定点整 数常量与整数变量相乘,然后再通过移位运算来确定小数点

你可能感兴趣的:(计算机体系结构笔记)