20145236《信息安全系统设计基础》第3周学习总结
第二章 信息的表示和处理
一、三种重要的数字表示
1.无符号数、有符号数、浮点数
-
正数的原码、反码以及补码是其本身。
-
负数的原码是其本身,反码是对原码除符号位之外的各位取反,补码则是反码加1。
2.为什么用补码表示
-
能够统一+0和-0的表示
-
采用原码表示,+0的二进制表示形式为0 000 0000,而-0的二进制表示形式为1 000 0000;
-
采用反码表示,+0的二进制表示形式为0 000 0000,而-0的二进制表示形式为1 111 1111;
-
采用补码表示,+0的二进制表示形式为0 000 0000,而-0的二进制表示形式为1 111 1111+1=1 0000 0000,因为计算机会进行截断,只取低8位,所以-0的补码表示形式为0000 0000。
-
补码的表示范围比原码和反码表示的范围都要大。用补码能够表示的范围为-128127,0127分别用0000000001111111来表示,而-127-1则用10000001~11111111来表示,多出的10000000则用来表示-128。
-
对于有符号整数的运算能够把符号位同数值位为一起处理
-
如果把符号位单独考虑的话,CPU指令还要特意对最高位进行判断,使计算机的最底层实现变得复杂。
3.整数溢出漏洞
参考资料:整数溢出与程序安全
-
一个整数是一个固定的长度,它能存储的最大值是固定的,当尝试去存储一个大于这个固定的最大值时,将会导致一个整数溢出。
-
整数溢出将会导致"不能确定的行为"。大多数编译器会忽略整数溢出,致使整数溢出没有立即察觉,因此没有办法去用一个应用程序来判断先前计算的结果在实际上是否也是正确的,可导致某些类型的bugs,缓冲区溢出等。
二、信息存储
1.字
-
每个计算机都有一个字长,指明整数和指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长最重要的系统参数就是虚拟地址空间的最大大小。
-
对于一个字长为w位的机器而言,虚拟地址的范围为0~2w-1,程序最多访问2w字节。
2.gcc -m32/64
-
当没有-m32或-m64参数时,一般情况下会生成跟操作系统位数一致的代码
-
gcc -m32 可以在64位机上(比如实验楼的环境)生成32位的代码
3.字节顺序
-
字节顺序是网络编程的基础,是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。
-
小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处。
-
大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处。
4.布尔代数
-
逻辑运算
-
结果是1或0
-
所有逻辑运算都可以用与、或、非表达(最大式、最小式)而与或非可以用“与非”或“或非”表达,所以,只要一个与非门,就可以完成所有的逻辑运算。
- 逻辑与(&&) 遇0为0;
- 逻辑或(||) 遇1为1;
- 逻辑非 遇0为1,遇1为0;
-
位运算
-
结果是位向量
- 按位与(&) 二进制每一位遇0为0;
- 按位或(|) 二进制每一位遇1为1;
- 按位异或(^) 00=0,01=1,10=1,11=0;
- 按位取反(~) 二进制每一位取反。
-
掩码运算
-
掩码是位运算的重要应用,这里掩码是一个特定位模式,表示从一个字中选择一个位的集合。对特定位可以置一,可以清零。
-
逻辑运算与掩码运算
-
逻辑运算结果是1或0,位运算结果是位向量
-
如果对第一个参数求值就能确定表达式的结果,逻辑运算符就不会对第二个参数求值。
三、整数表示
1.整型数据类型
- char 字符型数据,占用一个字节
- unsigned char 无符号字符型数据,占用一个字节
- short 短整形数据,占用两个字节
- unsigned short 无符号短整型数据,占用两个字节
- int 整形数据,占用两个字节
- unsigned int 无符号整型数据,占用两个字节
- long 长整型数据,占用四个字节
- unsigned long 无符号长整型数据,占用四个字节
- 数据类型long long是在ISO C99中引入的。(编译:gcc -std=c99)。
2.无符号数的编码
- 对于长度为w的位向量,都有一个唯一的值与之对应;反过来,在0~2^w-1之间的每一个整数都有一个唯一的长度为w的位向量二进制表示与之对应。
3.补码编码
-
有符号整数最常见的表示方法:二进制补码形式。
-
二进制补码形式的三个特点:
-
二进制补码的范围是不对称的:|TMin|=|TMax|+1,即不存在与最小值相对应的整数,这容易造成程序中细微的错误。
-
位数相同的前提下,无符号数的最大值刚好是二进制补码最大值的2倍加1:UMax=2TMax+1。
-
二进制补码中的-1与UMax有相同的位表示——全1位串。
ANSI C标准并未规定使用二进制补码形式来表示有符号整数,但是几乎所有的机器都是这么做的。补码利用寄存器长度固定的特性简化数学运算,将数学运算统一成加法,只要一个加法器就可以实现所有的数学运算。
-
4.有符号整数的另外两种标准表示方法
-
二进制反码形式:与二进制补码的表示方法类似,区别在于最高有效位的权值不同。
-
原码:最高有效位是符号位,确定剩下的位取负权值还是正权值。
-
这两种表示都有一个奇怪的属性,即对于数字0存在两种编码。对于两种方法,[00..0]都被解释成+0 ,而-0在二进制反码中表示为[11..1] ,在原码中表示为[10..0]。
-
虽然曾经有过基于二进制反码表示法的机器,但几乎所有现代机器都使用二进制补码。
-
符号数值编码方式使用在浮点数的表示中。
-
5.符号数与无符号数之间的转换
ANSI C规定在无符号整数和有符号整数之间进行强制类型转换时,并未改变对象的位模式,改变的是位模式的解释方式。处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,位模式不变。
T2U:补码到无符号数的转换
U2T:无符号数到补码的转换
-
有符号数转换为无符号数时,负数转换为大的正数(可以理解为原值加上2的n次方),而正数保持不变。
- x<0 T2Uw(x)=x+2^w
- x>0 T2Uw(x)=x+2^w
(*w表示数据类型的位数)
-
无符号数转换为有符号数时,对于小的数将保持原值,对于大的数则转换为负数(可以理解为原值减去2的n次方)
- u<2^(w-1) U2Tw(u)=u
- u>=2^(w-1) U2Tw(u)=u-2^w
(*w表示数据类型的位数)
6.C中有符号和无符号数
-
通常认为生命的常量是有符号的,要创建一个无符号常量,必须加上后缀字符'U'或者'u'。
-
C语言允许无符号数和有符号数之间的转换,转换的原则是底层的位保持不变。
- 显式转换:强制类型转换。
- 隐式转换:一种类型的表达式被赋值给另外一种类型的变量时。
-
C语言在处理同时包含无符号数和有符号数的表达式时,会隐式的将有符号数转换为无符号数,并假设这两个操作数都是非负的,然后执行运算。这一特性对于标准的算术运算来说无多大差异,但对于像"<"和">"这样的关系运算符,有时会导致和直觉不相符的结果。
例:当一个有符号数“-1”与无符号数“0”用关系运算符连接时,会自动的将-1隐式转换为无符号数4294967295(假设是一个使用补码的32位机器),负数变成了正数,“-1<0u”的结果值就是0。
- C语言的强制类型转换
7.扩展一个数字的位表示
-
零扩展:要将一个无符号数转换为一个更大的数据类型,只需简单的最高位前加0。
-
符号扩展:将一个补码数字转换为一个更大的数据类型,在表示中添加最高有效位值的副本。
8.截断数字
-
截断一个数字可能会改变其值,这也是值溢出的一种形式。,
-
无符号数x,将其截断成k位,mod 2^k。
-
有符号数x,先将其看作无符号数截断,然后在转换成有符号数。
四、 整数运算
1.无符号加法
-
"字长膨胀"
-
在一系列连续的加法操作中,前一次的运算结果和当前操作数的和超出了当前字长的表示范围,若要想正确保存本次的运算结果,必须增大字长。
-
持续的"字长膨胀"意味着要想完整的表示算术运算的结果,不能对字长做任何限制。一些编程语言,例如Lisp,支持无限精度的运算,允许任意字长的整数运算。更常见的是,编-
-
程语言只支持固定精度(字长)的运算。
-
无符号运算可以被视为某种形式的模运算。无符号加法等价与计算取模后的和,可以通过简单的丢弃超出字长的部分来完成取模计算。
-
算术运算溢出
-
指完整的整数结果无法放到数据类型的字长限制中。
-
执行C程序时,不会将溢出作为错误而发出警告信号。
-
判断无符号运算是否溢出,例如s=x+y(s、x、y均为无符号数),则唯一可靠的判断标准就是s<x或s<y。
-
模数加法构成了一种数学结构——阿贝尔群。可交换、可结合、单位元0、每个元素有加法逆元。
-
2.二进制补码加法
-
大多数计算机使用同样的机器指令来执行无符号或者有符号加法。
-
二进制补码加法在溢出时同样会将结果中超过字长的部分丢弃,然而,这样的结果在数学上却能用取模来等同。
-
以z=x+y为例,其中x,y,z均为位长为n的有符号数
- 正溢出:x+y >= 2^(w-1)
z=x+y-2^w x,y为正值,而最终结果z为负值。
- 负溢出:x+y < -2^(w-1)
z=x+y-2^w x,y为负值,而最终结果z为正值。
3.补码的非
-
补码非运算
-
对于范围在[-2(w-1),2(w-1))中的x,补码的非运算有如下两种情况:
-
x=-2(w-1)时,为-2(w-1)
-
x>-2^(w-1)时,为-x
-
-
位级补码非
-
对每一位求补,再对结果+1
-
设k为最右面的1的位置,将k左边的所有位取反。
4.乘法
-
C语言中的无符号乘法被定义为对整数乘积结果进行低位截断后的值,即等价于对乘法结果进行取模运算。
-
C语言中的有符号乘法是通过将2w位的乘积截断为w位的方式实现的,即mod 2^w。
-
对于无符号和补码乘法来说,乘法运算的位级表示都是一样的。
5.常数乘法
-
在大多数机器上,乘法指令和加法、减法、位运算、移位等指令相比要慢很多。因此,编译器使用的一项重要优化措施就是试图用移位和加法的组合来代替乘以常数因子的乘法。
-
常数为2的k次幂的时,直接左移k位即可。
-
常数不是2的整数次幂的时,将常数C表示为2的几个整数次幂的和,结合移位运算和加法运算。
-
对无符号变量x
- x<<k 等价于x*2^k。 (特别地,我们可以用1U<<k来计算2^k)
-
对二进制补码数x
- x<<k 等价于x*2^k。
-
即使在运算结果溢出时,上述等价关系依然成立。
6.除以2的幂
- 机器运算中,除法比乘法更慢。当被除数为2的整数次幂时,通过右移来解决。右移时需要区分无符号数和补码。
注意:整数除法总是舍入到零
-
无符号数——逻辑右移:无符号数除以2的k次幂,就等同于对其逻辑右移k位。
-
补码——算术右移:补码进行算术左移时,需要考虑补码数的正负,因为整数除法总是舍入到零,无符号数中没有负数不必担心,但补码中有正有负,正数向下舍入到零,负数应该向上舍入到零。所以这里涉及到在移位前偏置。
- 即:x≥0时,除以2的k次幂等价于将x算术右移k位
x<0时,先将x加上(2^k)-1,再算术右移k位
- 即:x≥0时,除以2的k次幂等价于将x算术右移k位
-
与乘法不同,这种右移方法不能推广到任意常数C。
7.整数运算的思考
-
计算机执行的“整数运算”实际上是一种模运算。
-
表示数字的有限字长限制了可能的值的取值范围。
-
无论运算数是以无符号形式还是补码形式表示,都有完全一样或者非常类似的位级行为。
五、 浮点数
1.概述
-
浮点表示对形如 V=x*2^y 的有理数进行编码
-
适用于:非常大的数字(|V|>>0)、非常接近于0的数字(|V|<<1)、实数运算的近似值。
-
IEEE浮点标准:IEEE标准754
2.二进制小数
-
二进制点左边第i位,权为2i;右边第i位,权为(1/2)i。
-
增加二进制表示的长度可以提高表示的精度。
3.IEEE浮点格式
- IEEE浮点标准
表示一个数: V=(-1)^s * M * 2^E
-
符号:s决定这个数是正还是负。0的符号位特殊情况处理。
-
尾数:M是一个二进制小数,范围为12-ε或者01-ε,ε=(1/2)^n.
-
阶码:E对浮点数加权,权重是2的E次幂(可能为负数)。
-
浮点数的位表示划分为三个字段,分别对这些值进行编码
-
一个单独的符号位 s 直接编码符号 s.
-
k位的阶码字段 exp = e(k-1)……e1e0编码阶码 E.
-
n位小数字段 frac = f(n-1)……f1f0编码尾数 M,但是编码出来的值也依赖于阶码字段的值是否等于0.
-
浮点数可分为三种表达方式
- 规格化值
- 非规格化值
- 特殊值
1)规格化的值
exp的位模式既不全0也不全1的时候,这是最一般最普遍的情况,因而是规格化的。
- 阶码字段被解释为以偏置形式表示的有符号整数。
阶码: E = e-Bias
-
e:无符号整数
-
Bias:偏置值,Bias=[2^(k-1)-1]
-
小数字段frac的解释为描述小数值f,二进制小数点在小数字段最高有效位的左边。
-
尾数 M = 1+f(有时也可称作隐含的以1开头的表示)
2)非规格化的值
阶码域全为0时的数。
-
阶码: E = 1-Bias
-
尾数: M = f(小数字段的值,不包含隐含的1)
-
功能:
-
提供了一种表示数值0的方法。
-
表示那些非常接近0.0的数,提供一种“逐渐溢出”的属性。
-
3)特殊值
特殊值是在阶码位全为1的时候出现的。
-
小数域全为0时,数值用于表示无穷:符号位为0表示正无穷,符号位为1表示负无穷。
-
小数域非全0时,数值用于表示"NaN"(Not a Number)。这样的数值用于表示诸如对(-1)^0.5这样无意义的结果。
4.舍入
-
因为表示方法限制了浮点数的范围和精度,浮点运算只能近似的表示实数运算。
-
舍入运算的任务:找到和数值x最接近的匹配值x',可以用期望的浮点形式表示出来。
-
IEEE浮点格式定义了四种不同的舍入方法:
- 向偶舍入(默认) 将数字向上或向下舍入,是的结果的最低有效数字为偶数。能用于二进制小数。
- 向零舍入 把整数向下舍入,负数向上舍入。
- 向下舍入 正数和负数都向下舍入。
- 向上舍入 正数和负数都向上舍入。
-
默认的(即向偶舍入)方法可以得到最接近的匹配,其余三种产生实际值的确界。
5.浮点运算
-
浮点加法
- 可交换
- 不具结合性:缺少的重要群属性
- 大多数值的浮点加法都有逆元,除了无穷和NaN。
- 满足单调性
-
浮点乘法
- 可交换的
- 不具有结合性:可能发生溢出,或由于舍入而失去精度。
- 乘法单位元为1.0
- 在加法上不具备分配性
- 在一定条件下满足单调性(无符号或补码的乘法没有这些单调性属性)
6.C语言中的浮点数
-
C提供两种浮点类型:float和double,在支持IEEE浮点标准的机器上分别表示单精度和双精度浮点数。
-
C标准并未明确要求机器在浮点数的表示上采取IEEE标准 。
-
int、float、double之间的强制类型转换
- int → float 数字不会溢出,但有可能舍入
- int/float → double 保留精确的数值
- double → float 由于精确度变小,可能溢出为±∞,也有可能被舍入
- float/double → int 可能向零舍入,可能溢出。
代码执行
- p24 进制转换
- 这个代码在编译时出现了问题,结果检查后发现是输入编译命令的时候文件名输入错误!一个大写的尴尬!!!
- 以12345为例,输出如下:
- 用GDB单步跟踪练习2.11
教材习题解答
2.4
0x503c+0x8 = 0x5044
0x503c-0x40 = 0x4ffc
0x503c+64 = 0x5070
2.6
- 写出0x00359141、0x4a564504的二进制表示。并移动相对位置使其尽量匹配
0x00359141 = 0000 0000 0011 0101 1001 0001 0100 0001(2进制)
0x4a564504 = 0100 1010 0101 0110 0100 0101 0000 0100(2进制)
0000 0000 001101011001000101000001
0100 1010 010101100100010100000100
共有21位相匹配。整数基本上所有有效位都嵌在浮点数中。
2.8
- a = [01101001],b = [01010101]。计算:
~a = [10010110]
~b = [10101010]
a&b = [01000001]
a |b = [01111101]
a^b = [00111100]
2.11
- 在
inplace_swap
函数的基础上,写一段代码实现一个数组的头尾元素依次交换
代码如下:
#include<stdio.h>
#define MAX 10
void inplace_swap(int *x,int *y)
{
*y = *x^*y;
*x = *x^*y;
*y = *x^*y;
}
void reverse_array(int a[], int cnt)
{
int first,last;
for(first = 0,last = cnt-1;first<=last;first++,last--)
inplace_swap(&a[first], &a[last]);
}
void main()
{
int a[MAX];
int count,i;
printf("please enter the amount of numbers( no more than %d):\n",MAX);
scanf("%d",&count);
printf("please enter numbers('e' as the end)\n");
for(i = 1;i<=count;i++)
{
scanf("%d\n",&a[i-1]);
}
printf("the original array is as follow:\n");
for(i = 1;i<=count;i++)
{
printf("%d ",a[i-1]);
}
reverse_array(a, count);
printf("the new array is as follow:\n");
for(i = 1;i<=count;i++)
{
printf("%d ",a[i-1]);
}
}
- 以1,2,3,4和1,2,3,4,5作为输入的时候,结果如下:
- 结论:
当数组长度为奇数的时候,输出的结果最中间的数字变为0。原因是在最后一次调用inplace_swap的时候,传入的first和last都是原数组中最中间的数字;在第一处*y = x^y时,y指向的数字就变为了0.此后,0变作为最中间的数字参与循环。解决办法:将循环条件中的first<=last 改为first<last(最中间的数字不会参与循环)。
2.13
- 假设有两个函数实现位设置bis和位清除bic操作;只利用这两个函数实现按位 | 和^操作。
int bis(int x, int y);
int bis(int x, int y);int bool_or(int x,int y)
{
int result = bis(x,y);
return result;
}
int bool_xor(int x,int y)
{
int result = bis(bic(x,y),bic(y,x));
return result;
}
2.23
- 假设在一个采用补码运算的32位机器上执行这些函数。计算下列输入参数之后的结果:
#include<stdio.h>
int fun1(unsigned word)
{
return (int)((word<<24)>>24);
}
int fun2(unsigned word)
{
return ((int)word<<24)>>24;
}
void main()
{
int word;
printf("please enter a number:\n");
scanf("%d",&word);
printf("the result of fun1:%d\n",fun1(word));
printf("the result of fun2:%d\n",fun2(word));
}
- 结论:
w:0x00000076 fun1(w)=0x00000076,fun2(w)=0x00000076
w:0x87654321 fun1(w)=0x00000021,fun2(W)=0x00000021
w:0x000000c9 fun1(w)=0x000000c9 fun2(w)=0xffffffc9
2.25
- 以下代码试图计算数组a[]中所有元素的和,然而当参数length=0时,会发生存储器错误。试解释原因并修改代码。
#include<stdio.h>
#define MAX 100
float sum_elements(float a[], unsigned length)
{
int i;
float result = 0;
for (i =0;i<=length-1;i++)
{
result+=a[i];
}
return result;
}
void main()
{
float a[MAX];
unsigned number;
int i;
printf("Please enter the amount of numbers in your array:\n");
scanf("%u",&number);
if(number <0)
{
printf("Wrong!\n");
return;
}
if(number == 0)
{
printf("the result is:%f\n",sum_elements(a, number));
return;
}
else
{
printf("Please enter the elements:(the tail of array should be end by 'e')\n");
for(i = 0;i<=number-1;i++)
{
scanf("%f\n",&a[i]);
}
printf("the result is:%f\n",sum_elements(a, number));
return;
}
}