《深入理解计算机系统》阅读笔记--程序的机器级

压入和弹出栈数据

 

最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据。

栈是一种数据结构,可以添加和删除值,不过要遵循后进先出的原则,通过push操作将数据压入栈中,通过pop删除数据。

它具有一个属性:弹出的值永远是最近被压入而且仍然在栈中的值。

 

pushq指令的功能是把数据压入栈上,而popq是弹出数据,这些指令都只有一个操作数--压入的数据源和弹出的数据目的

 

将一个四字值压入栈中,首先要将栈指针减8,然后将值写入到新的栈顶地址

因为栈和程序代码以及其他形式的程序数据都是存放在同一个内存中,所以程序可以用标准的内存寻址方法访问栈内的任意位置。

 

  • 3.2 程序编码
    • 3.2.1 机器级表示
      • 抽象
        • 计算机系统使用了多种不同形式的抽象, 利用更简单的抽象模型来隐藏实现的细节
        • 最重要的两种抽象:
          1. 由指令集体系结构(Instruction Set Architecture, ISA)来定义机器级程序的格式和行为.
          2. 机器级程序使用的内存地址是虚拟地址, 提供的内存模型看上去是一个非常大的字节数组
      • 处理器状态
        • 程序计数器(通常被称为"PC", 在x86-64中用%rip表示): 给出将要执行的下一条指令在内存中的地址
    • 3.2.2 代码示例
      • 汇编与反汇编程序
        • 汇编: gcc
          • linux> gcc -Og -S mstore.c
            • 这会使GCC运行编辑器, 产生一个汇编文件mstore.s, 但是不做其他进一步的工作. (通常情况下, 它还会继续调用汇编器产生目标代码文件)
          • linux> gcc -Og -c mstore.c
            • 这会产生目标代码文件mstore.o, 它是二进制格式的, 所以无法直接查看
        • 反汇编: objdump
          • linux> objdump -d mstore.o
            • 这些反汇编程序(像objdump)会根据机器代码产生一种类似汇编代码的格式
      • 链接器
        • l链接器(Linker)是一个程序, 将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件
    • 3.2.3 关于格式的注释
      • 以"."开头的行
        • 都是指导汇编器和链接器工作的伪命令
      • 把C语言和汇编语言结合起来
        • 方法:
          1. 我们可以编写完整的函数, 放进一个独立的汇编文件中, 让汇编器和链接器把它和用C语言编写的代码合并起来
          2. 我们可以使用GCC的内联汇编(inline assembly)特性, 用asm伪指令可以在C程序中包含简短的汇编代码
            • 好处: 减少代码量
            • 坏处: 会使代码与某些特殊的机器有关,所以只应该在想要的特性只能以这种方式才能访问到时才使用它
      • 条件码: 条件码是CPU根据运算结果由硬件设置的位, 体现当前指令执行结果的各种状态信息. 例如: 算术运算产生的正/负/零/溢出等结果.
        • 奇偶标志(parity flag):
          • 定义: 用于反映运算结果中"1"的个数的奇偶性
          • 作用: 为了提供传送的可靠性, 如果采用奇偶校验的方法, 就可使用该标志位
  • 3.3 数据格式
    • 由于是从16位体系结构扩展成32字, Intel用术语"字(word)"表示16位数据类型. 因此32位数为"双字(double words)", 称64位数为"四字(quad words)".
    • 数据格式的汇编后缀:
      • 后缀"l": 表示4字节整数和8字节双精度浮点数, 这样不会产生歧义, 因为浮点数使用的是一组完全不同的指令和寄存器
  • 3.4 访问信息
    • 通用目的寄存器:
      • 作用: 用来存储整数数据和指针
      • 数量: 16
      • 详情:
        • 8位: | %al | %bl | %cl | %dl | %sil | %dil | %bpl | %spl | %r8b | %r9b | %r10b | %r11b | %r12b | %r13b | %r14b | %r15b |
        • 16位: | %ax | %bx | %cx | %dx | %si | %di | %bp | %sp | %r8w | %r9w | %r10w | %r11w | %r12w | %r13w | %r14w | %r15w |
        • 32位: | %eax | %ebx | %ecx | %edx | %esi | %edi | %ebp | %esp | %r8d | %r9d | %r10d | %r11d | %r12d | %r13d | %r14d | %r15d |
        • 64位: | %rax | %rbx | %rcx | %rdx | %rsi | %rdi | %rbp | %rsp | %r8 | %r9 | %r10 | %r11 | %r12 | %r13 | %r14 | %r15 |
          • 不同字节数的操作码可以访问不同的最低寄存器
    • 3.4.1 操作数指示符
      • 操作数: 一个操作中要使用的源数据值, 以及放置结果的目的位置
        • 操作数本身没有数据类型的标志, 它的数据类型由操作码确定
      • 操作数格式:
        • 产生原因: 源数据值可以以常数形式给出, 或是从寄存器或内存中读出. 结果可以存放在寄存器或内存中. 因此, 不同的操作数可以分为三种类型:
          1. 立即数: 用来表示常数值
          2. 寄存器: 表示某个寄存器的内容
          3. 内存引用: 根据计算出来的==地址==(通常是有效地址)访问某个内存位置
    • 3.4.2 数据传送指令
      • 注意点:
        • movabsq: 源操作数只能为64位立即数, 目的操作数只能是寄存器
        • 指令如何修改目的寄存器的高字节数: 按照操作码的不同, 有不同的方式. 有些操作码会保持高位不变, 有些会将高位设置为0, 有些会将高位设置为符号位
        • x86-64的限制: 传送指令的两个操作数不能都指向内存位置
        • movq只能以表示32位的补码数字的立即数作为源操作数, 然后把这个值符号扩展为64位的值, 放到目的寄存器
        • 立即数不能做目的操作数
    • 3.4.4 压入和弹出栈数据
      • 寄存器%rsp:
        • 作用: 栈指针寄存器, 用来存放栈指针的值
  • 3.5 算术和逻辑操作

    • 3.5.1 加载有效地址(load effective address: leaq)
      • 作用: 将一个有效地址写入目的寄存器
        • 有效地址: 计算出来的地址
      • 目的操作数: 必须是寄存器
    • 3.5.2 一元和二元操作
      • 一元操作: 只有一个操作数, 既是源又是目的. 这个操作数可以是一个寄存器或者内存地址
      • 二元操作: 其中第二个操作数既是源又是目的, 第一个操作数可以是立即数/寄存器/内存位置. 第二个操作数是寄存器或内存地址
        • 注意: 当第二个操作数是内存地址时, 处理器必须从内存读出值, 执行操作, 再把结果写回内存
    • 3.5.5 特殊的算术操作

      • imulq:

        • 一般用法:
        imulq   %rdx, %rcx
        
        • 特殊用法:
        imulq   %rdx     //有符号乘法
        mulq   %rdx     //无符号乘法
        
          - 作用: 计算两个64位值的全128位乘积
          - 注意点: 要求一个参数必须在寄存器%rax中, 而另一个作为指令的操作源给出. 然后乘积存放在寄存器%rdx(高64位)和%rax(低64位)中
        
  • 3.6 控制
    • 3.6.1 条件码
      • 常用条件码:
        • CF(Carry Flag): 进位标志
        • ZF(Zero Flag): 零标志
        • SF(Sign Flag): 符号标志
        • OF(Overflow Flag): 溢出标志
      • CMP: cmp系列指令会比较两个操作数并设置条件码,而不改变目的寄存器
      • 条件码的改变:
        • 绝大部分指令的执行都会设置条件码
        • lea系列指令不改变条件码
          • 原因: lea系列是用来进行地址计算的
    • 3.6 2 访问条件码
      • SET: set系列指令可以根据条件码的某种组合, 将一个字节设置为0或1
      • 溢出:
        • 负溢出: 当计算结果本应该是负数但寄存器中却存放的是正数时为负溢出
        • 正溢出: 当计算结果本应该是正数但寄存器中却存放的是负数时为正溢出
        • 详见2.3.2 补码加法
    • 3.6.4 跳转指令的编码
      • 汇编: 将汇编代码转化成机器码
      • 反汇编: 将机器码转化成汇编代码
      • 理解跳转指令目标的如何编码, 对第7章研究链接非常重要
        • 汇编代码中: 跳转目标用符号标号书写
        • 汇编器, 以及后来的链接器, 会产生跳转目标的适当编码
          • 金沙官网线上,跳转指令有几种编码, 通常使用PC相对寻址(PC-relative)
            • PC-relative: 会将目标指令的地址与紧跟在跳转指令后面的那条指令的地址之间的差作为编码
              • - 优点: 1. 使用PC-relative时, 当使用链接器将这些代码重新定位时, 跳转目标的编码并没有改变 2. 指令编码很简洁 3. 目标代码可以不做改变的移动到内存的不同位置
            • "绝对"地址: 用4个字节直接指定目标

二、历史

 

Inter的处理器系统俗称x86,第一代处理器是8086,一个单芯片,16位微处理器,主要为 IBM PC 和 DOS 设计,有 1MB 的地址空间。八年后的 1985,第一个 32 位 Intel 处理器(IA32) 386 诞生。2004 年,奔腾(Pentium) 4E 成为了第一个 64 位处理器(x86-64)。2006 年 Core 2 成为了第一个多核 Intel 处理器。

 

五、访问信息

 

一个x86-64的中央处理单元包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针如下图:

金沙官网线上 1

这里的名字都是以%r开头 ,不过后面也包含了一些不同的命名规则,这是历史演化造成的。

最早的8086中有8个16位的寄存器,即上图中的%ax到%bp,当扩展到IA32架构时,这些寄存器也扩展成了32位寄存器,标号从%eax到%ebp,当扩展到x86-64后,原来的8个寄存器扩展为64位,标号从%rax到%rbp,除此之外还增加了8个新的寄存器,标号从%r8到%r15

 

数据传送指令

 

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令,最简单形式的数据传送指令是MOV类,MOV类由四条指令组成:movb,movw,movl和movq.  b,w,l,q分别是1、2、4和8字节

金沙官网线上 2

源操作数指定的值是一个立即数,存储在寄存器中或者内存中,目的操作数指定一个位置,要么是一个内存地址。而在x86-64中增加一个限制,传送指令的两个操作数不能都指向内存位置。

金沙官网线上 3

上图中记录的是两类数据移动指令,在将较小的源值赋值到较大的目的的时候使用,所有这些指令都把数据从源(在寄存器或内存中)复制到目的寄存器。MOVZ 类中的指令把目的中剩余的字节填充为0而MOVS类中的指令通过符号扩展来填充,把源操作的最高位进行复制

 

一、为什么要学习和了解汇编

 

编译器基于编程语言的规则,目标机器的指令集和操作系统遵循的惯例,经过一系列的阶段生成机器代码。GCC c语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编和链接器,根据汇编代码生成可执行的机器代码。这一章节其实就是来更加深入的认识和理解汇编代码

 

现在我们更多接触的都是一些高级语言,如JAVA,GO,Python,其实用这些语言的时候,更大程度上,已经屏蔽了一些程序的细节,即机器级的实现。但是如果是用汇编语言,程序员就必须制定程序用来执行计算的低级指令。

 

那么为什么我们还要学习和了解汇编呢? 虽然现在编译器已经替我们做了生成汇编代码的大部分工作,但是作为程序员,如果我们能够阅读和理解汇编代码将是一个非常重要的技能,好处是:

能够理解编译器的优化能力分析代码中隐含的低效率

如我们通过线程包写并发程序时,了解不同线程是如何共享程序数据或保持数据私有的,以及准确知道如何在哪里访问共享数据,这些在机器代码都是可见的

 

本文由金沙官网线上发布于操作系统,转载请注明出处:《深入理解计算机系统》阅读笔记--程序的机器级

您可能还会对下面的文章感兴趣: