深入理解计算机系统:03---信息的表示和处理(信息的存储:十六进制表示法、字长、字节顺序(大小端)、字符串/机器代码/布尔代数的表示、位级运算/逻辑运算/移位运算)

一、几个重要的概念

  • 大多数计算机使用 8 位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位
  • 机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识,称为它的地址(address)。所有可能地址的集合就称为虚拟地址空间(virtual address space)。 顾名思义,这 个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现(见后面“虚拟内存”文章)是将动态随机访问存储器(DRAM)。闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为 程序提供一个看上去统一的字节数组
  • 在接下来的几篇文章中,我们将讲述编译器和运行时系统是如何将存储器空间划分为更可管理的单元,来存放不同的程序对象(program object),即程序数据、指令和控制信息。 可以用各种机制来分配和管理程序不同部分的存储。这种管理完全是在虚拟地址空间里完 成的。例如,C 语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序 对象)都是某个存储块的第一个字节的虚拟地址。C 编译器还把每个指针和类型信息联系起来,这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置 处的值。尽管 C 编译器维护着这个类型信息,但是它生成的实际机器级程序并不包含关于数据类型的信息
  • 每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列

二、十六进制表示法

  • 个字节由 8 位组成。在二进制表示法中,它的值域是 00000000~11111111。如果看 成十进制整数,它的值域就是0~255。两种符号表示法对于描述位模式来说都不是非常方便。二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦
  • 替代的方法是, 以 16 为基数,或者叫做十六进制(hexadecimal)数,来表示位模式。十六进制(简写为“hex”)使用数字'0'~'9',以及'A'~'F'来表示16个可能的值下图展示了16个十六进制数字对应的十进制值和二进制值。用十六进制书写,一个字节的值域为00~FF

  • 在 C 语言中,以0x 或0X 开头的数字常量被认为是十六进制的值。字符'A'~'F'既可以是大写,也可以是小写。例如,我们可以将数字 FA1D37B写作0XFA1D37B,或者 0xfald37b,甚至是大小写混合,比如,0xFalD37b
  • 编写机器级程序的一个常见任务就是在位模式的十进制、二进制和十六进制表示之间人工转换

进制之间的转换

  • 编写机器级程序的一个常见任务就是在位模式的十进制、二进制和十六进制表示之间人工转换

二进制与十六进制间的转换

  • 二进制和十六进制之间的转换比较简单直接,因为可以一次执行一个十六进制 数字的转换。数字的转换可以参考下面的图片。一个简单的窍门是,记住十六进制 数字 A、C 和 F 相应的十进制值。而对于把十六进制值 B、D 和 E 转换成十进制值,则可 以通过计算它们与前三个值的相对关系来完成
  • 比如,假设给你一个数字 0x173A4C。可以通过展开每个十六进制数字,将它转换为 二进制格式,如下所示:

  • 反过来,如果给定一个二进制数字 1111001010110110110011,可以通过首先把它分为 每 4 位一组来转换为十六进制。不过要注意,如果位总数不是 4 的倍数,最左边的一组可以少于 4 位,前面用 0 补足。然后将每个 4 位组转换为相应的十六进制数字:

十六进制与十进制之间的转换

  • 十进制和十六进制表示之间的转换需要使用乘法或者除法来处理一般情况
  • 将一个十 进制数字x转换为十六进制,可以反复地用 16 除x,得到一个商q和一个余数r,也就是 x=g*16+r。 然后,我们用十六进制数字表示的 r 作为最低位数字,并且通过对q反复 进行这个过程得到剩下的数字。例如,考虑十进制 314 156 的转换:

  • 反过来,将一个十六进制数字转换为十进制数字,我们可以用相应的 16 的幂乘以每 个十六进制数字。比如,给定数字 0x7AF,我们计算它对应的十进制值为7*16^{2} +10* 16 +15=7*256+10*16+15=1792+160+15=1967

三、字数据大小

  • 每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。因为 虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为0~2^{w}-1, 程序最多访问2^{w}个字节

32位系统、64位系统

  • 最近这些年,出现了大规模的从 32 位字长机器到 64 位字长机器的迁移。这种情况首先出现在为大型科学和数据库应用设计的高端机器上,之后是台式机和笔记本电脑,最近则出现在 智能手机的处理器上。32 位字长限制虚拟地址空间为 4 千兆字节(写作 4GB),也就是说,刚刚超过 4*10^{9}字节。扩展到 64 位字长使得虚拟地址空间为 16EB,大约是 1.84*10^{9}字节。
  • 大多数 64 位机器也可以运行为 32 位机器编译的程序,这是一种向后兼容。因此,举例来说,当程序 prog.c 用如下伪指令编译后,可以在 32 位或 64 位机器上正确运 行
gcc -m32 prog.c
  • 另一方面,若程序用下述伪指令编译,那就只能在 64 位机器上运行
gcc -m64 prog.c
  • 因此,我们将程序称为“32 位程序”或 “64 位程序”时,区别在于该程序是如何编译的, 而不是其运行的机器类型(重点)

编码的数据格式

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

C语言编码的数据格式

  • C 语言支持整数和浮点数的多种数据 格式。下图展示了为C语言各种数据类型分配的字节数(我们在下一篇文章讨论 C 标准保证的字节数和典型的字节数之间的关系)
  • 有些数据类型的确切字节数依赖于程序是如何被编译的。我们给出的是 32 位和 64 位程序 的典型值。整数或者为有符号的,即可以表示负数、零和正数;或者为无符号的,即只能表示非负数

  • C 的数据类型 char 表示一个单独的字节。尽管“char”是由于它被用来存 储文本串中的单个字符这一事实而得名,但它也能被用来存储整数值
  • 数据类型 short、int 和 long 可以提供各种数据大小。即使是为 64 位系统编译,数据类型 int 通常也只有 4 个字节。数据类型 long —般在 32 位程序中为 4 字节,在 64 位程序中则为 8 字节
  • 为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99 引人了 一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其中就有数据类型 int32_t 和 int64_t,它们分别为 4 个字节和 8 个字节。使用确定大小的整数类型是程序 员准确控制数据表示的最佳途径
  • 大部分数据类型都编码为有符号数值,除非有前缀关键字 unsigned 或对确定大小的 数据类型使用了特定的无符号声明。数据类型 char 是一个例外。尽管大多数编译器和机器将它们视为有符号数,但 C 标准不保证这一点。相反,正如方括号指示的那样,程序员 应该用有符号字符的声明来保证其为一个字节的有符号数值。不过,在很多情况下,程序 行为对数据类型 char 是有符号的还是无符号的并不敏感
  • 对关键字的顺序以及包括还是省略可选关键字来说,C 语言允许存在多种形式。比 如,下面所有的声明都是一个意思:

  • 上上图还展示了指针(例如一个被声明为类型为“char*”的变量)使用程序的全字长
  • 大多数机器还支持两种不同的浮点数格式:单精度(在 C 中声明为 float)和双精度 (在 C 中声明为 double)。这些格式分别使用 4 字节和 8 字节
  • 程序员应该力图使他们的程序在不同的机器和编译器上可移植。可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。C 语言标准对不同数据类型的数字范围设置了下界(这点在后面还将讲到), 但是却没有上界。因为从 1980 年左右到 2010 年左右,32 位机 器和 32 位程序是主流的组合,许多程序的编写都假设为图 2-3 中 32 位程序的字节分配。随 着 64 位机器的日益普及,在将这些程序移植到新机器上时,许多隐藏的对字长的依赖性就 会显现出来,成为错误。比如,许多程序员假设一个声明为 int 类型的程序对象能被用来存储 一个指针。这在大多数 32 位的机器上能正常工作,但是在一台 64 位的机器上却会导致问题

四、寻址和字节顺序(大小端)

  • 对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序 列,对象的地址为所使用字节中最小的地址。例如,假设一个类型为 int 的变量 x 的地址 为 0x100,也就是说,地址表达式 的值为0x100。那么,(假设数据类型 int 为 32 位表 示)x的4个字节将被存储在内存的 0x100、0x101、0x102 和 0x103 位置

大小端

  • 某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效 字节的顺序存储:
    • 前一种规则最低有效字节在最前面的方式,称为小端法(little endian)
    • 后一种规则最高有效字节在最前面的方式,称为大端法(big endian)
  • 假设变量 x 的类型为 int,位于地址 0x100 处,它的十六进制值为 0x01234567。地 址范围 0x100~0x103 的字节顺序依赖于机器的类型:

  • 注意,在字 0x01234567 中,高位字节的十六进制值为 0x01,而低位字节值为 0x67
  • 大多数 Intel 兼容机都只用小端模式。另一方面,IBM 和 Oracle(从其 2010 年收购 Sun Microsystems 开始)的大多数机器则是按大端模式操作。注意我们说的是“大多数”。 这些规则并没有严格按照企业界限来划分。比如,IBM 和 Oracle 制造的个人计算机使用 的是 Intel 兼容的处理器,因此使用小端法。许多比较新的微处理器是双端法(bi-endian), 也就是说可以把它们配置成作为大端或者小端的机器运行。然而,实际情况是:一旦选择 了特定操作系统,那么字节顺序也就固定下来。比如,用于许多移动电话的 ARM 微处理 器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统 Android(来自 Google)和 IOS(来自 Apple) 却只能运行于小端模式

“端”的起源

  • 令人吃惊的是,在哪种字节顺序是合适的这个问题上,人们表现得非常情绪化。实际 上,术语 “little endian(小端)” 和 “big endian(大端)” 出自 Jonathan Swift 的《格利佛游记》一书,其中交战的两个派别无法就应该从哪一端(小端还是大端) 打开一个半熟的鸡蛋达成—致。就像鸡蛋的问题一样,选择何种字节顺序没有技术上的理 由,因此争论沦为关于社会政治论题的争论。只要选择了一种规则并且始终如一地坚持, 对于哪种字节排序的选择都是任意的。

字节顺序可能会造成问题的场景

  • 对于大多数应用程序员来说,其机器所使用的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到同样的结果。不过有时候,字节顺序会成为问题
  • ①首先是在不同类型的机器之间通过网络传送二进制数据时:
    • 一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时,接收程序会发现,字里的字节成了反序的。 为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以 确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示
    • 可参阅:https://blog.csdn.net/qq_41453285/article/details/89109337
  • ②第二种情况是,当阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检査机器级程序时。作为一个示例,从某个文件中摘出了下面这行代码,该文件给出了一 个针对 Intel X86-64 处理器的机器级代码的文本表示

  • 这一行是由反汇编器(disassembler)生成的,反汇编器是一种确定可执行程序文件所表示 的指令序列的工具。我们将在第 3 章中学习有关这些工具的更多知识,以及怎样解释像这 样的行。而现在,我们只是注意这行表述的意思是:十六进制字节串 01 05 43 0b 20 00 是 一条指令的字节级表示,这条指令是把一个字长的数据加到一个值上,该值的存储地址由 0X200b43 加上当前程序计数器的值得到,当前程序计数器的值即为下一条将要执行指令 的地址。如果取出这个序列的最后 4 个字节:43 Ob 20 00, 并且按照相反的顺序写出,我 们得到 00 20 0b 43。去掉开头的 0,得到值 0x200b43,这就是右边的数值。当阅读像此 类小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。书写字节序 列的自然方式是最低位字节在左边,而最高位字节在右边,这正好和通常书写数字时最高 有效位在左边,最低有效位在右边的方式相反
  • ③字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在 C 语言 中,可以通过使用强制类型转换(cast)或联合(union)来允许以一种数据类型引用一个对 象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说是非常有用,甚至是必需的

大小端测试

  • 下面展示了一段C代码,它使用强制类型转换来访问和打印不同程序对象的字节表示:
    • typedef 将数据类型 byte_pointer 定义为一个指向类型为“unsigned char”的对象的指针。这样一个字节指针引用一个字节序列,其中每个字节都被认为是一 个非负整数
    • show_bytes的输入是一个字节序列的地址,它用一个字节指针以及一个字节数来指示。该字数指定为数据类型size_t,表示数据结构大小的首选数据类型
#include <stdio.h>

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, size_t len)
{
    size_t i;
    
    // #.2x表明整数必须用至少两个数字的十六进制格式输出
    for (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 *));
}
  • 在几种不同的机器上运行如下面所示的代码,得到如下图所示的结果。我们使用了以下几种机器:
    • Linux 32:运行 Linux 的 Intel IA32 处理器
    • Windows:运行 Windows 的 Intel IA32 处理器
    • Sun:运行 Solaris 的 Sun Microsystems SPARC 处理器(这些机器现在由 Oracle 生产)
    • Linux 64:运行 Linux 的 Intel x86-64 处理器
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);
}

  • 参数 12 345 的十六进制表示为 0x00003039。对于 int 类型的数据,除了字节顺序以 外,我们在所有机器上都得到相同的结果。特别地,我们可以看到在 Linux 32、Windows 和 Linux 64 上,最低有效字节值 0x39 最先输出,这说明它们是小端法机器;而在 Sun 上 最后输出,这说明 Sum 是大端法机器。同样地,float 数据的字节,除了字节顺序以外, 也都是相同的。另一方面,指针值却是完全不同的。不同的机器/操作系统配置使用不同 的存储分配规则。一个值得注意的特性是 Linux 32、Windows 和 Sun 的机器使用 4 字节 地址,而 Linux 64 使用 8 字节地址
  • 可以观察到,尽管浮点型和整型数据都是对数值 12 345 编码,但是它们有截然不同的字节模式:整型为 0x00003039,而浮点数为 0X4640E400。一般而言,这两种格式使用不 同的编码方法。如果我们将这些十六进制模式扩展为二进制形式,并且适当地将它们移 位,就会发现一个有 13 个相匹配的位的序列,用一串星号标识出来:

  • 这并不是巧合。当我们研究浮点数格式时,还将再回到这个例子。

五、字符串的表示

  • C 语言中字符串被编码为一个以 null(其值为 0)字符结尾的字符数组。每个字符都由 某个标准编码来表示,最常见的是 ASCII 字符码。因此,如果我们以参数“12345”和 6 (包括终止符)来运行例程 showJsytes�我们得到结果 31 32 33 34 35 00。请注意,十进 制数字 x 的 ASCII 码正好是 0x3x, 而终止字节的十六进制表示为 0x00。在使用 ASCII 码 作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文 本数据比二进制数据具有更强的平台独立性

六、代码的表示

  • 考虑下面的C函数:
int sum(int x, int y)
{
    return x + y;
}
  • 当我们在示例机器上编译时,生成如下字节表示的机器代码:

  • 我们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方 式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植
  • 计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调试的辅助表以外。在“程序的机器级表示”中学习机器级编程时,我们将更清楚地看到这一点

七、布尔代数

八、位级运算

  • 附加链接:https://blog.csdn.net/qq_41453285/article/details/88753266
  • C 语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使 用的那些符号就是 C 语言所使用的:| 就是 OR(或),& 就是 AND(与), 〜就是 NOT(取 反), 而^就是 EXCLUSIVE-OR(异或)。 这些运算能运用到任何“整型”的数据类型上(例如上面那张C语言数据类型中所指定的类型)
  • 以下是一些对 char 数据类型表达式求值的:

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

掩码运算

  • 位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合
  • 让我们来看一个例子,掩码 0xFF(最低的8位为 1)表示一个字的低位字节。位级运算 x&0xFF 生成一个由 x 的最低有效字节组成的值,而其他的字节就被置为0。比如,对于  x= 0x89ABCDEF,其表达式将得到0x000000EF
  • 表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个 32 位机器来说,同样的掩码可以写成0xFFFFFFFF,但是这样的代码不是可移植的

九、逻辑运算

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

  • 可以观察到,按位运算只有在特殊情况下,也就是参数被限制为 或者 1 时,才和与其对应的逻辑运算有相同的行为
  • 逻辑运算符&&和||与它们对应的位级运算&和|之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此, _ 例如,表达式 a&&5/a 将不会造成被零除,而表达式 p&&*p++也不会导致间接引用空指针

十、移位运算

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/107592060