我们如何与计算机进行沟通?(二)

1.写在前面

上篇博客中我们介绍如何和计算机进行沟通的一些简单的内容,讲了一些简单的指令。这篇博客我们继续讲后面的内容,这个系列的博客应该会写三篇。今天开始我们的第二篇的内容。

2.人机交互

计算机的发明是为了数字计算,但很快被用于商业方面的文本处理。当前大多数计算机使用字节来表示字符,也就是每个人都遵循的表示方法ASCII。

在这里插入图片描述

大写和小写字母恰好相差32,这个观察可以用作检查或切换大小写的捷径。

一系列指令可以从双字中提取一个字节,因此对双字的加载和存储足以传输字节和字。但由于某些程序中文本的流行,所以RISC-V提供了字节转移指令。加载无符号字节(lbu)指令从内存加载了一个字节,将其放在寄存器的最右边8位。存储字节(sb)指令从寄存器的最右边8位取一个字节并将其写入内存。因此,我们复制一个字节的顺序如下:

lbu x12,0(x10)
sb x12,0(x11)

字符通常组合成具有可变数量的字符串。字符串的表示有三种选择:

  • 字符串的第一个位置保留,用于给出字符串的长度;
  • 附加带有字符串长度的变量;
  • 字符串的最后位置用一个字符标记字符串结尾。

C语言使用第三种选择,使用值为0的字节终止字符串。因此,字符串"Cal"在C语言中用以下4个字节表示。

RISC-V指令系统具有加载和存储这种16位半字的指令。load half unsigned(加载无符号半字)从内存中读取一个半字,将它放在寄存器的最右边16位,用零填充最左边的48位。与加载字节一样,加载半字(lh)将半字视为有符号数,因此进行符号扩展以填充寄存器的最左边48位。存储半字(sh)从寄存器的最右边16位取半字并将其写入内存。

3.对大立即数的RISC-V编址和寻址

虽然将所有RISC-V指令保持32位长可以简化硬件,但又是使用32位或更大的常量或地址会很方便。

3.1大立即数

虽然常量通常很短并且适合12位字段,但有时它们也会更大。

RISC-V指令系统包括load upper immediate(取立即数高位,lui),用于将20位常数加载到寄存器的第31位到第12位。将第31位的值复制填充到最左边32位,最右边的12位用0填充

3.2分支中的寻址

RISC-V分支指令使用称为SB型的RISC-V指令格式。这种格式可以表示从-4096到4094的分支体质,以2的倍数便是。由于最近的一些原因,它只能跳转到偶数地址。SB型格式包括一个7位操作码、一个3位功能码、两个5位的寄存器操作数(rs1和rs2)和一个12位地址立即数。该地址使用特殊的编码方式,简化了数据通路设计,但使组装变得复杂。下面这条指令:

bne x10,x11,2000 //if x10 != x11 go to location 2000

可以组装为这种格式:

在这里插入图片描述

其中条件分支的操作码是1100111,而bne的funct3码是001

无条件跳转-链接指令(jal)是唯一使用UJ型格式的指令。该指令由一个7位操作码、一个5位目标寄存器操作数(rd)和一个20位地址立即数组成。链接地址,即jal之后的指令地址,被写入rd中。

与SB型格式一样,UJ型格式的地址操作数使用特殊的立即数编码方式,它不能编码奇数地址。

jal x0,2000 // go to location 2000

被组装为这种格式:

在这里插入图片描述

如果程序的地址必须适合这个20位字段,则意味着没有程序可能大于2^20,而这对于今天的需求来说太小,不是一个现实的选择。另一种方法是指定一个与分支地址偏移量相加的寄存器,以便分支指令可以按如下来计算:

程序计数器 = 寄存器内容 + 分支地址偏移量

条件分支指令在循环和if语句中使用,因此它们倾向于转移到附近的指令。

由于程序计数器包含当前指令的地址,如果我们使用PC作为该寄存器,可以在距离当前指令的±210个字的地方分支,或者跳转到距离当前指令±218个字的地方。几乎所有循环和if语句都小于2^10个字,因此PC是理想的选择。这种形式的寻址方式称为PC相对寻址。

但是不能保证跳转的指令就是一定小于2^18个字的距离,因为不能保证被调用者接近调用者。因此RSIC-V允许双指令序列来非常长距离地跳转到任何32位地址:lui将地址的第12位至第31位写入临时寄存器,jalr将地址的低12位加到临时寄存器并跳转到目标位置。

3.3RISC-V寻址模式总结

多种不同的寻址形式通常称为寻址模式,具体的如下:

在这里插入图片描述

  1. 立即数寻址,操作数是指令本身的常量
  2. 寄存器寻址,操作数在寄存器中。
  3. 基址或偏移寻址,操作数于内存中,其地址是寄存器和指令中的常数之和。
  4. PC相对寻址,分支地址是PC和指令中常量之和。

3.4机器语言译码

有时必须通过逆向工程将机器语言恢复到初始的汇编语言。

在这里插入图片描述

在这里插入图片描述

4.指令和并行性:同步

当任务之间相互独立时,并行执行更为容易,但通常任务之间需要协作。协作通常意味着一些任务正在写入其他任务必须读取的值。需要知道任务何时完成写入以便其他任务安全地读出,因此任务之间需要同步。如果它们不同步,则存在数据竞争的危险,那么程序的结果会根据事件发生的次序而改变。

数据竞争:如果来自两个不同的线程访存请求访问同一个位置,至少有一个是写,且连续出现,那么这两次存储访问形成了数据竞争。

在计算机中,同步机制通常由用户级的软件例程所构建,而这依赖于硬件提供的同步指令。加锁和解锁同步操作的实现。加锁和解锁可直接用于创建只有单个处理器可以操作的区域,称为互斥区,以及实现更复杂的同步机制。

在多处理器中实现同步所需的关键是一组硬件原语,能够提供以原子方式读取和修改内存的单元的能力。也就是说,在内存单元的读取和写入之间不能插入其他任何操作。如果没有这样的能力,构建基本同步原语的成本将会提供,并且随着处理器数量的增加而急剧增加。

有许多基本硬件原语的实现方案,所有这邪恶都提供了原子读和原子写的能力,以及一些判断读写是否是原子操作的方法。通常,体系结构设计人员不希望用户使用基本的硬件原语,而是期望系统程序员使用原语来构建同步库。

我们从原子交换原语开始,考虑两个处理器尝试同时进行交换操作:这种竞争会被阻止,因为其中一个处理器首先执行交换,并返回0,而第二个处理器在进行交换时将返回1。使用交换原语实现实现同步的关键是操作的原子性,交换时不可分割的,硬件将对两个同时的交换进行排序。尝试以这种范式设置同步变脸的两个处理都不可能认为它们同时设置变量。

可以在使用指令对,其中第二条指令返回一个值,该值表示该指令对是否被原子操作。如果任何处理器执行的所有其他操作都发生在该对指令之前或之后,则该指令对实际上是原子的。因此,当指令对实际上是原子操作时,没有其他处理器可以在指令之间改变值。

在RISC-V中,这对指令指的是一个称为保留加载双字(lr.d)的特殊加载指令和一个称为条件存储双字(sc.d)的特殊存储指令。这些指令按序使用:如果保留加载指令指定的内存位置的内容在条件存储指令执行到统一地址之前发生了变化,则条件存储指令失败且不会将值写入内存。条件存储指令定义为将寄存器的值存储在内存中,如果成功则将另一个寄存器的值更改为0,如果失败则更改为非零值。因此sc.d指定了三个寄存器:一个用于保存地址,一个用于指示原子操作失败或成功,还有一个用户如果成功则将值存储在内存中。由于保留加载指令返回初始值,并且条件存储指令仅在成功时返回0。

again:lr.d x10,(x20) //load-reserved
      sc.d x11,x23,(x20) //store-condititonal
      bne x11,x0,again //branch if store fails
      addi x23,x10,0 //put loaded value in x23

没当处理器干预并修改lr.d和sc.d指令之间的内存中的值时,sc.d都会将非零值写入x11,从而导致代码序列重新执行。在此序列结束时,x23的值和x20指向的内存位置的值发生了原子交换。

保存加载/条件存储机制的一个优点是可以用于构建其他的同步原语,这些同步原语的实现需要在lr.d和sc.d之间插入更多指令,但不会太多。由于条件存储会在另一个store尝试加载保留地址或异常之后失败,因此必须注意选择在两个指令之间插入哪些指令。特别是,只有保留加载/条件存储块中的整点算术、前后分支和后向分支被允许执行且不会出现问题。否则,可能会产生死锁情况,由于重复的页错误,处理器永远无法完成sc.d。此外,保留加载和条件存储之间的指令应该很少,以将由于不相关事情和竞争处理器导致条件存储频繁失败的可能性降至最低。

5.翻译并启动程序

在这里插入图片描述

5.1编译器

编译器是将C程序转换为机器能理解的符号形式—汇编语言程序。

5.2汇编器

由于汇编语言是高层软件的接口,因此汇编器还可以处理机器指令的常见变体,就像这些变体是它自己的指令一样。硬件不需要实现这些指令;然而,它们在汇编语言中的出现简化了程序转换和编程。这类指令称为伪指令。

总之,伪指令为RISC-V提供了比硬件实现更丰富的汇编语言指令系统。

汇编器也会接受不同基数的数字。除了二进制和十进制之外,它们通常接受比二进制更简短又很容易转换为位模式的基数。汇编器将汇编语言程序转换为目标文件,该目标文件是机器指令、数据和将指令正确放入内存所需信息的组合。

为了在汇编语言程序中产生每条指令的二进制版本,汇编器必须确定与所有标签相对应的地址。汇编器会跟踪分支中使用的标签和符号表中的数据传输指令。

UNIX系统的目标文件通常包含六个不同的部分:

  • 目标文件头,描述了目标文件的其他部分的大小和位置。
  • 代码段,包含机器语言代码。
  • 静态数据段,包含在程序生命周期内分配的数据。
  • 重定位信息,标记了在程序加载到内存时依赖于绝对地址的指令和数据
  • 符号表,包含剩余的未定义的标签。
  • 调试信息,包含有关如何编译目标模块的简明描述,以便调试器可以将机器指令与C源文件相关联并使数据结构可读。

5.3链接器

对一个程序的一行进行单一更改需要编译和汇编整个程序。完全重新编译是对计算资源的严重浪费。这种重复对于标准库程序来说尤其浪费,因为程序员将要编译和汇编根据定义几乎永远不会改变例程。另一种方法是独立编译和汇编每个过程,因此更改一行代码只需要编译和汇编一个过程。这种替代方案需要一个新的系统程序,称为链接编辑器或链接器。它将所有独立汇编的机器语言程序缝合在一起。

链接器有用的原因是修正代码要比重新编译和重新汇编快的多。主要的步骤如下:

  1. 将代码和数据模块按符号特征放入内存。
  2. 决定数据和指令标签的地址。
  3. 修改内部和外部应用。

链接器使用每个对象模块中的重定位信息和符号表来解析所有未定义的标签。这些引用发生在分支指令和数据地址中,因此该程序的工作与编辑器的工作非常相似:它找到旧地址并用新地址替换它们。

如果解析了所有外部引用,则链接器接下来将确定每个模块将占用的内存位置。由于文件是单独汇编的,因此汇编器无法知道模块的指令和数据相对于其他模块的位置。当链接器将模块的指令和数据相对于其他模块的位置。当链接器将模块放入内存时,必须重定位所有的绝对引用以反映其真实地址。

链接器生成可在计算机上运行的可执行文件。通常,此文件具有与目标文件相同的格式,但它不包含任何未解析的引用。具有部分链接的文件是可能的。

5.4加载器

现在可执行文件在磁盘上,操作系统将其读取到内存并启动它。加载器在UNIX系统中遵循以下步骤:

  1. 读取可执行文件首部以确定正文段和数据段的大小。
  2. 为正文和数据创建足够大的地址空间。
  3. 将可执行文件中的指令和数据复复制到内存中。
  4. 将主程序的参数(如果有)复制到栈顶。
  5. 初始化处理器寄存器并将栈指向第一个空闲位置。
  6. 跳转到启动例程,将参数复制到参数寄存器中并调用程序的主例程。当主例程返回时,启动例程通过exit系统调用终止程序。

5.5动态链接库

程序在运行之前链接库文件的传统方法。虽然这种静态方法是调用库例程的最快的方法,但有一些缺点:

  • 库例程成为可执行代码的一部分。如果发布了修复错误或支持新硬件设备的新版本库,则静态链接程序仍将继续使用旧版本。
  • 它会加载执行过程中在任何位置可能会调用的所有库的的所有例程,即使有些例程不一定会用到。相对于程序,库可能很大。

这些缺点引出了动态链接库,其库例程在程序运行之前不会被链接和加载。程序和库例程都保存有关非局部过程及其名字的额外信息。在DLL的最初版本中,加载器运行一个动态链接程序,使用文件中的额外信息来查找相应的库并更新所有外部引用。

DLL初始版本的缺点是,它仍然链接了可能被调用的库的所有例程,而不是在程序运行期间调用的哪些例程。这种观察引出了DLL的延迟过程链接版本,其中每个例程仅在被调用之后才链接。

在这里插入图片描述

第一次调用库例程时,程序调用虚入口并执行间接跳转。这个跳转指向一段代码,它将一个数字放入寄存器来识别所需库例程,然后跳转到动态链接器/加载器。链接器/加载器找到所需的例程。重新映射它,并更改间接跳转位置中的地址以指向该例程。然后跳转到这个例程。例程完成后,它将返回到初始调用点。此后,它都会间接跳转到该例程而不需额外的中间过程。

总之,DLL需要额外的空间来存储动态链接所需的信息,但不要求复制或链接整个库。它们在第一次调用例程时会付出大量的开销,但此后只需要一个间接跳转。请注意,从库返回不会产生额外的开销。

5.6启动Java程序

上面主要描述了执行程序的传统模型,重点在于以特定指令系统体系结构甚至体系结构的特定是吸纳为目标的程序的快速执行。

在这里插入图片描述

Java不是编译成目标计算机的汇编语言,而是首先编译成易于解释的指令:java字节码指令系统。

称为Java虚拟机的软件解释器可以执行java字节码。解释器是一个模拟指令系统体系结构的程序。

解释的优点是可移植性。Java虚拟机软件的可用性意味着大多数人可以在Java发布后不久编写和运行Java程序。

解释的缺点是性能较低。

6.写在最后

本篇博客主要简单的介绍了下C程序和Java程序如何在计算机中运行的。下篇博客继续讲指令(X86)。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值