深入理解计算机系统(CSAPP):第二章.信息的表示和处理

现代计算机存储和处理的信息以二进制信号表示,这些微不足道的二进制,或者称之为位(bit),形成了数字革命。

目录

2.1 信息存储

2.1.1 十六进制表示法

2.1.2 字数据大小

2.1.3 寻址和字节顺序

2.1.4 表示字符串

2.1.5 表示代码

2.1.6 布尔代数的简介

2.1.7 C语言中的位级运算 


熟知的十进制起源于印度,在12世纪被阿拉伯数学家改进,并在13世纪被意大利数学家带到西方。

但当构造存储和处理信息的机器时,二进制值工作的更好。二进制能够更容易的被表示、存储和传输,例如,可以表示为穿孔卡片上有洞或者无洞、导线上的高电压或者低电压,或者顺时针逆时针的磁场。对二值信号进行存储和执行计算的电子电路非常简单和可靠。

本章主要研究三种最重要的数字表示:

  1. 无符号:无符号编码基于传统的二进制表示法,表示大于或者等于零的数字
  2. 补码:补码编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字
  3. 浮点数:浮点数编码是表示实数的科学计数法的以2为基数的版本。

计算机的表示法是有限数量的位来对应一个数字编码,因此,当结果太大以至于不能表示时,某些运算会基础(overflow)。益处会导致令人吃惊的后果。

在现在大多数机器上(使用32位表示数据类型int),计算表达式200*300*400*500 会得出 -884901888。

由于计算机表示的精度有限,浮点数运算是不可结合的。例如,在大多数机器上,C表达式(3.14+1e20)-1e20 求得的值会是0.0,然而正确值确实3.14.

整数运算和浮点数运算会有不同的数学属性是因为他们处理数字表示有限性的方式不同

  1. 整数:整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示时精确的
  2. 浮点数:浮点数表示方式可以编码一个较大的数值范围,但是这种表示只是近似的

2.1 信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是单独访问内存中单独的位。机器级程序将内存视为一个非常大的字节数字,称之为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称之为地址。所有可能地址的集合就称之为虚拟内存地址空间。

顾名思义,这个虚拟内存地址空间只是一个展现给机器级程序的概念性映像。实际的实现是由动态随机访问存储器、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。

2.1.1 十六进制表示法

一个字节由8位组成。在二进制表示法中,他的值域是00000000~11111111。如果把它看成十进制整数,值域0~255。两种符号表示法对于描述位模式来说都不是非常方便。二进制表示法冗长,十进制表示与为模式互相转化很麻烦。替代的方式是十六进制表示法。

0x39A7F8 ------> 0011 1001 1010 0111 1111 1000 
1100 1001 0111 1011 ------> 0xC97B
0xD5E4C -------> 1101 0101 1110 0100 1100
10 0110 1110 0111 1011 0101 ------>0x26E7B5 

当值x是2的非负整数n次幂时,也就是x = 2^n,我们可以很容易的将x写成十六进制形式,只要记住x的二进制表示就是1后面跟n个0。.

所以当n表示成 i+4x的形式,其中0≤i≤3,我们就可以把x写成十六进制的数。

i=0i=1i=2i=3
1248

例如 2048 = 2^11,我们可以写成 11 = i+4x ,i=3 x=2; 从而得到十六进制数 0x800。

在2^n中 可以拆解成 n=i+4x ,已知一个数是2^n,可以通过式子,快速求得该数的十六进制数。

i 表示 十六进制开头数,而x则表示后面零的个数

n2^n(十进制)2^n(十六进制)
95120x200
195242880x80000
14163840x4000
16655360x10000
171310720x20000
5320x20
71280x80

一般情况下,十进制和十六进制之间的转换需要使用乘法或者处罚来处理。

将十进制数字x 转换为十六进制,x=q*16+r,商q ,余数r,在将q当做x 反复做上述运算。

例如,将314156 转换成十六进制

314156 = 19634 * 16 + 12
19634 = 1227 * 16 + 2
1227 = 76 * 16 + 11
76 = 4 * 16 + 12
4 = 0 * 16 + 4

十六进制数为:0x4CB2C

给定数字0x7AF,转换成十进制

x = 7 * 16^2 + 10 * 16 + 15
x = 1792 + 160 + 15
x = 1967
十进制二进制十六进制
00000 00000x00
1671010 01110xA7
620011 11100x3E
1881011 11000xBC
550011 01110x37
1361000 10000x88
2591111 00110xF3
820101 00100x52
1721010 11000xAC
2311110 01110xE7

2.1.2 字数据大小

每台机器都有一个字长(Word size) ,指明指针数据的标称大小(nominal size),因为虚拟地址是以这样的一个字来进行编码的,所以字长决定了最重要的系统参数——虚拟地址空间的最大大小。

对于一个字长为ω位的机器而言,虚拟地址的范围为0~ 2^ω-1,程序最多访问2^ω个字节。

计算机和编译器支持多种不同的方式的编码格式,如不同长度的整数和浮点数。比如,许多机器都有处理单个字节的执行,也有处理表示为2字节、4字节或者8字节整数的指令,还有些支持表示为4字节和8字节的浮点数

C声明字节数
有符号无符号32位64位
[singed] charunsigned char11
shortunsigned short22
int unsigned44
long unsigned long48
int32_tuint32_t44
int64_tuint64_t88
char * 48
float44
double88

整数或者为有符号的,即可以表示负数、零和正数;或为无符号的,即只能表示非负数。C的数据类型表示一个单独的字节。尽管“char” 是由于他被用来存储文本串的单个字符这一事实而得名,但他也能被用来存储整数数值。数据类型short 、int和long可以提供各种数据的大小。

2.1.3 寻址和字节顺序

对于跨越多字节的程序对象,我们必须建立两个规则:

  1. 这个对象的地址是什么?
  2. 在内存中如何排列这些字节?

在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。

例如,假设一个数据类型为int的x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么x的4个字节将被存储在 0x100、0x101、0x102 和 0x103. 位置。

排列表示一个对象的字节有两个通用的规则。考虑一个ω位的整数,其位表示为[x(ω-1),x(ω-2),`````,x1,x0],假设ω是8的倍数,则其高位有效字节为[x(ω-1),x(ω-2),x(ω-3),x(ω-4),x(ω-5),x(ω-6),x(ω-7),x(ω-8)],最低有效字节包括[x7,x6,x5,x4,x3,x2,x1,x0],其他字节包含中间的位。某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。

前一种——最低有效字节在最前面的,我们称之为小端法

第二种——最高有效字节在最前面的,我们称之为大端法

假设数据类型int的变量x ,位于地址0x100处,他的十六进制为0x01234567,地址为0x100~0x103的字节顺序依赖于机器类型

大端法

······0x1000x1010x1020x103······
01234567

小端法

``````0x1000x1010x1020x103``````
67452301

注意:在字0x01234567中,高位字节为十六进制0x01低位字节为0x67

在大多数intel兼容机器都只用小端模式。另一方面,IBM和Oracle的大多数机器则是按照大端模式操作。比如IBM和Oracle制造的个人计算机使用intel来兼容的处理器,因此使用小端法。许多比较新的微处理器则是 双端法,也就是说可以把它们配置成作为大端或者小端的机器运行。然而,实际情况是,一旦选择了特定的操作系统,那么字节顺序也就固定下来了。比如移动电话的ARM微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统——Android和iOS 却只能运行小端。

但是在实际情况下,哪种方式更合适的问题上,争论不休,就像无法从哪个方向打开一个半熟的鸡蛋一样无法达成一致。如何选择字节顺序没有技术上的理由。

不同模式下所产生的问题

在大多数情况下,其机器所使用的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到相同的结果。不过有时候字节顺序会成为问题。首先是在不同的类型的机器之间通过网络传送二进制数据时,一个常见的问题是当小端法机器产生的数据被发送大大端法机器 ,或者反过来时,接收程序会发现,字里的字节成了反序的,为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则。

第二种情况,当阅读表示整数的字节序列顺序也很重要。这通常发生在检查机器级程序时。

4004d3: 01 05 43 0b 20 00 add %eax,0x200b43(%rip)

代码是由反汇编器生成的,反汇编器是一种确定可执行程序文件所表示的指令序列的工具。十六进制字符串 01 05 43 0b 20 是一条指令的字节级表示,这条指令是把一个字长的数据加到一个值上。该值的存储地址由 0x200b43加上当前程序计数器的值得到。

由小端法可知 整数的序列为 43 0b 20 00 。可知 x= 00 20 0b 43  则x=0x200b43 

字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在C语言中可以通过强制类型转换(cast) 或者联合(union) 来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同,大多数应用编程都强烈不推荐这种编码技巧。

#include<stdio.h>

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start,size_t len){
    size_t i;
    for(int i=0;i<len;i++)
       printf(" %.2x",start[i];
    printf("\n");
}

void show_int(int x){
    show_bytes((byte_pointer) &x,sizeof(int));
}

void show_float(float x){
    show_bytes((byte_pointer) &x,sizeof(float));
}

void show_pointer(void *x){
    show_bytes((byte_pointer) &x,sizeof(void *));
}

过程show_int、show_float和show_pointer展示了如何使用程序show_bytes来分别输出类型为int、float和void *的C程序对象的字节表示。

在程序中,仅仅传递给show_bytes一个指向参数x的指针&x,并且这个指针被强制类型转换为“unsigned char* ” 。这种强制类型转换告诉编译器,程序应该将这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象,然后这个指针会被看成是对象使用的最低字节地址。

sizeof(T) 返回存储一个类型为T的对象所需要的字节数。

void test_show_bytes(int val){
    int ival = val;
    float fval = (float)ival;
    int *pval = &ival;
    show_int(ival);
    show_float(fval);
    show_pointer(pval);
}

根据不同的机器 上述输出的值略有差异

机器类型类型字节(十六进制)
Linux3212345int39 30 00 00 
12345.0float00 e4 40 46
&ivalint *e4 f9 ff bf
Windows12345int39 30 00 00 
12345.0float00 e4 40 46
&ivalint *b4 cc 22 00
sun(大端法)12345int00 00 30 39
12345.0float46 40  e4 00
&ivalint *ef ff fa 0c
Linux6412345int39 30 00 00 
12345.0float00 e4 40 46
&ivalint *b8 11 e5 ff ff 7f 00 00

int 和float 只有排列顺序不同,但是指针值确是完全不同的。另一方面 linux64 使用8字节地址,linux32 和Windows 使用4字节地址;

可以观察到,尽管浮点型和整数型都是对数值12345编码,但他们有完全不同的编码模式:

        整数型:0x 00003039,而浮点型则为:0x4640E400,一般而言两种不同的数据类型使用不同的编码方法,如果将这些十六进制模式扩展成二进制形式,并且适当的移位,就会发现一个有13个相匹配的位的序列

当我们研究浮点数时在回到这个例子; 

思考下面对show_bytes的三次调用

int val = 0x87654321;
byte_pointer valp = (byte_pointer)&val;
show_bytes(valp,1);
show_bytes(valp,2);
show_bytes(valp,3);
大端法小端法
8721
87 65 21 43
87 65 4321 43 65

2.1.4 表示字符串

C语言中字符被编码为一个以null字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。因此我们以参数“12345” 和“6”(包括终止符)来运行实例show_bytes,我们得到的结果是 31 32 33 34 35 00 。

十进制的ASCII编码正好是0x3x,而终止字符的十六进制表示为0x00,在使用ASCII编码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因此,文本数据比二进制数据具有更强的平台独立性。

2.1.5 表示代码

        

int sum(int x,int y){
    return x + y;
}

当我们在示例机上编译时,生成字节表示的机器代码

Linux32 55 89 e5 8b 45 0c 03 45 08 c9 c3 
Windows 55 89 e5 8b 45 0c 03 45 08 5d c3 
Sun     81 c3 e0 08 90 02 00 09
Linux64 55 48 89 e5 89 7d fc 89 75 f8 03 45 fc c9 c3 

我们发现指令编码是不同的,不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植

2.1.6 布尔代数的简介

二进制值是计算机编码、存储和操作信息的核心,所以围绕数值0和1的研究已经演化出了丰富的数学知识体系。

布尔注意到通过将逻辑值TRUE和FALSE编码为二进制1和0,能够设计出一种代数,以研究逻辑推理的基本原则

~
01
10

&01
000
101

|01
001
111

^01
001
110

2.1.7 C语言中的位级运算

C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:| 就是OR(或) ,& 就是AND(与) ,~ 就是 NOT(取反),而 ^ 就是 EXCLUSIVE-OR(异或)。这些运算能运用到任何“整型”的数据类型上

C二进制结果十六进制
~0x41~[0100 0001]1011 11100xBE
~0x00~[0000 0000]1111 11110xFF
0x69 & 0x55[0110 1001]&[0101 0101]0100 00010x41
0x69 | 0x55[0110 1001]|[0101 0101]0111 11010x7D

正如示例说明的那样,确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并且执行二进制运算,然后在转换成十六进制。

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 inplace_swap(int *x,int* y){
/* *x = a ; *y = b */
    *y = *x ^ *y; /* *x = a; *y = a^b */
    *x = *x ^ *y; /* *x = a^a^b, *y =a^b */
    *y = *x ^ *y; /* *x = b, *y = b^a^b = a */
}

A:对于一个长度为奇数的数组,长度cnt=2k+1,函数reverse_array 最后一个循环中,变量first和last的值分别是什么?

        first = last = k

B:为什么调用inplace_swap会将数组元素设置为0? 

        因为当first = last 时  *x = a ,*y =a 当a^a时  结果为0

C:对reverse_array的代码做哪些简单改动,就能消除这个问题?

        first < last

位级运算的一个常见用法就是掩码运算,这个掩码运算是一个位模式,表示从一个字中选出的位的合集。让我们来看一个例子,掩码0xFF(最低位的8位为1)表示一个字的低位字节。

位级运算 x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。

比如,对于x=0x89ABCDEF,,其表达式将得到0x000000EF。表达式~0将生成一个全为1的掩码。不管机器的字大小是多少。尽管对于一个32位机器来说,同样的掩码可以写成0xFFFFFFFF,但这样的代码是不可移植的

例题 当x=0x87654321 ω = 32时 ,我们给出了求值的结果,请给出运算过程

  1. x的最低有效字节,其他位置均为0。【0x00000021】  x&0xFF
  2. 除了x的最低有效字节外,其他的位都取补,最低有效字节保持不变。【0x789ABC21】x^(~0xFF)
  3. x的最低有效字节设置成全1,其他字节保持不变。【0x876543FF】 x | 0xFF

2.1.8 C语言中的逻辑运算

C语言还提供了一组逻辑运算符 || 、&& 和 ! ,分别对应命题逻辑中的OR、AND和 NOT 运算。逻辑运算很容易和位级运算混淆,但他们的功能完全不同。逻辑运算认为所有的非零的参数都表示TRUE,而参数0表示FALSE。他们返回1或者0,分别表示结果为TRUE或者为FALSE。

表达式逻辑
!0x410x00
!0x000x01
!!0x410x01
0x69 && 0x550x01
0x69 || 0x55 0x01

可以观察到,按位运算只有在特殊情况下,也就是参数被限制为0或者为1时,才和与其对应的逻辑运算有相同的行为。

逻辑运算符&& 和|| 与他们对应的位级运算符 &和 | 之间第二个重要的区别就是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此,例如 a&&5/a将不会造成被零除,而表达式p&&*p++也不会导致简介引用空指针。

例题2.14:假设x 和y的字节值分别是0x66和0x39。

表达式表达式
x&y0x20x&&y0x01
x|y0x7Fx||y0x01
~x|~y0xDF!x||!y0x01
x & !y0x00x && ~y0x01

例题2.15:只使用位级和逻辑运算,编写一个C表达式,他等价于 x==y。换句话说,当x和y相等时他将返回1,否则返回0。

  ! (x^y)

2.1.9 C语言中的移位运算

C语言当中还提供了一组移位运算,想左或者向右移动位模式。对于一个位表示为[x(ω-1),x(ω-2),......,x(0)]的操作数x,C表达式x<<k 会生成一个值,其位表示为[x(ω-k-1),x(ω-k--2),......,x(0),0,......,0]。也就是说,x向左移动了k位,丢弃最高位的k位,并且在右端补k个0.移位量应该是一个0~ω-1之间的值。移位运算是从左至右可结合的,所以x<<j<<k等价(x<<j)<<k

有一个相应的右移运算x>>k,但是他的行为有些微妙。一般而言,机器支持两种方式的右移:逻辑右移和算数右移。

逻辑右移在左端补k个0,得到的结果是[0,......0,x(ω-1),x(ω-2),...x(k)]。

算数右移是在左端补k个最高有效位的值,得到的结果是[x(ω-1),x(ω-1),x(ω-1),x(ω-2),...x(k)]。这种做法看上去可能有点奇特,但是我们会发现他对有符号整数数据的运算非常有用。

操作

参数x

[01100011] [10010101]
x<<4[00110000] [01010000]
x>>4(逻辑右移)[00000110] [00001001]
x>>4(算数右移)[00000110] [11111001]

斜体的数字表示的是最右端或最左端填充的值。可以看出 其他的填充都是0,只有一个 最高位为1的算数右移 填充数为1。

C语言中并没有明确定义对于有符号数应该使用哪种类型的右移——算数右移或者逻辑右移都可以。不幸的是,这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。

然而,几乎所有的编译器、机器组合都对有符号数使用算数右移,且许多程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。

与C语言相比,java对于如何进行右移有了明确的定义。

表达式 x>>k 表示算数右移

表达式 x>>>k 表示逻辑右移

对于许多机器上,当移动一个ω位的值时,移位指令只考虑位移量的低log2(ω)位,因此实际上位移量就是通过计算(k mod ω) 得到的。例如 ,当 ω=32时,偏移量 = k mod 32。不过这种行为对于C程序来说是没有保证的,所以应该保持位移量小于待移位置的位数。

另一方面,Java特别要求位移数量应该按照我们所讲的求模公式来计算。

【使用教程】 一、环境配置 1、建议下载anaconda和pycharm 在anaconda中配置好环境,然后直接导入到pycharm中,在pycharm中运行项目 anaconda和pycharm安装及环境配置参考网上博客,有很多博主介绍 2、在anacodna中安装requirements.txt中的软件包 命令为:pip install -r requirements.txt 或者改成清华源后再执行以上命令,这样安装要快一些 软件包都安装成功后才算成功 3、安装好软件包后,把anaconda中对应的python导入到pycharm中即可(不难,参考网上博客) 二、环境配置好后,开始训练(也可以训练自己数据集) 1、数据集准备 需要准备yolo格式的目标检测数据集,如果不清楚yolo数据集格式,或者有其他数据训练需求,请看博主yolo格式各种数据集集合链接:https://blog.csdn.net/DeepLearning_/article/details/127276492 里面涵盖了上百种yolo数据集,且在不断更新,基本都是实际项目使用。来自于网上收集、实际场景采集制作等,自己使用labelimg标注工具标注的。数据集质量绝对有保证! 本项目所使用的数据集,见csdn该资源下载页面中的介绍栏,里面有对应的下载链接,下载后可直接使用。 2、数据准备好,开始修改配置文件 参考代码中data文件夹下的banana_ripe.yaml,可以自己新建一个不同名称的yaml文件 train:训练集的图片路径 val:验证集的图片路径 names: 0: very-ripe 类别1 1: immature 类别2 2: mid-ripe 类别3 格式按照banana_ripe.yaml照葫芦画瓢就行,不需要过多参考网上的 3、修改train_dual.py中的配置参数,开始训练模型 方式一: 修改点: a.--weights参数,填入'yolov9-s.pt',博主训练的是yolov9-s,根据自己需求可自定义 b.--cfg参数,填入 models/detect/yolov9-c.yaml c.--data参数,填入data/banana_ripe.yaml,可自定义自己的yaml路径 d.--hyp参数,填入hyp.scratch-high.yaml e.--epochs参数,填入100或者200都行,根据自己的数据集可改 f.--batch-size参数,根据自己的电脑性能(显存大小)自定义修改 g.--device参数,一张显卡的话,就填0。没显卡,使用cpu训练,就填cpu h.--close-mosaic参数,填入15 以上修改好,直接pycharm中运行train_dual.py开始训练 方式二: 命令行方式,在pycharm中的终端窗口输入如下命令,可根据自己情况修改参数 官方示例:python train_dual.py --workers 8 --device 0 --batch 16 --data data/coco.yaml --img 640 --cfg models/detect/yolov9-c.yaml --weights '' --name yolov9-c --hyp hyp.scratch-high.yaml --min-items 0 --epochs 500 --close-mosaic 15 训练完会在runs/train文件下生成对应的训练文件及模型,后续测试可以拿来用。 三、测试 1、训练完,测试 修改detect_dual.py中的参数 --weights,改成上面训练得到的best.pt对应的路径 --source,需要测试的数据图片存放的位置,代码中的test_imgs --conf-thres,置信度阈值,自定义修改 --iou-thres,iou阈值,自定义修改 其他默认即可 pycharm中运行detect_dual.py 在runs/detect文件夹下存放检测结果图片或者视频 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值