CSAPP 学习笔记「程序的机器级表示」

#Computer

其实这章2天也就看完了,但碰上清明节放假,我拖延了很久的时间才来记录笔记。

程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时只要求程序员能直接用汇编语言编写程序,现在则是要求他们能阅读和理解编译器产生的代码。

###程序编码

机器级编程使用了两种重要的抽象:

  • 机器级程序的格式和行为,定义为指令集体系结构,它定义了处理器状态、指令的格式、以及每条指令对状态的影响。
  • 机器级程序使用的存储器是虚拟地址,操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。

在命令行上生成 C 语言源程序的汇编代码:

unix> gcc -O1 -S code.c

O1表示一级优化。

关于汇编代码格式:

  • ATT 格式,这是 GCC 的默认格式。
  • Intel 格式,学校使用的是这种格式。

###数据格式

Intel 用术语「字」来表示 16 位数据类型。因此,32 位数为「双字」。

下图为 C 语言数据类型在 IA32 中的大小:

C 声明 Intel 数据类型 汇编代码后缀 大小(字节)
char 字节 b 1
short w 2
int 双字 l 4
long int 双字 b 4
char * 双字 l 4
float 单精度 s 4
double 双精度 l 8

###访问信息

一个 IA32 中央处理器包含一组8个32位值的寄存器:%eax, %ecx, %edx, %ebx, %esi, %edi, %esp(栈指针), %ebp(帧指针)

操作数的指示符有多种类型:

  • 立即数,也就是常数值
  • 寄存器,表示某个寄存器的内容
  • 存储器引用,它会根据计算出来的地址访问某个存储器位置
  • Imm(Eb, Ei, s),这表示的是最常用的形式。四个组成部分分别为:一个立即数偏移 Imm, 一个基址寄存器 Eb, 一个变址寄存器 Ei 和一个比例因子 s,这里 s 必须是 1、2、4或者8。然后有效地址被计算为:Imm + R[Eb] + R[Ei] * s

数据传送指令有:

  • MOV S, D
  • MOVS S, D
  • MOVZ S, D
  • pushl S
  • popl D

IA32有一条限制,传送指令的两个操作数不能都指向存储器位置。

MOVS 和 MOVZ 指令类都是讲一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展 (MOVS) 或者零扩展(MOVZ)。

在 IA32 中,栈顶元素的指针是所有栈中元素地址中最低的,而 栈指针 %esp 保存着栈顶元素的地址。

###算术与逻辑操作

指令 效果 描述
leal S, D D <- &S 加载有效地址
INC D D <- D + 1 加 1
DEC D D <- D - 1 减 1
NEG D D <- -D 取负
NOT D D <- ~D 取补
ADD S, D D <- D + S
SUB S, D D <- D - S
IMUL S, D D <- D * S
XOR S, D D <- D ^ S 异或
OR S, D D <- D | S
AND S, D D <- D & S
SAL S, D D <- D << k 左移
SAR S, D D <- D >> a k 算术右移
SHR S, D D <- D >> l k 逻辑右移

###控制

除了整数寄存器,CPU 还维护着一组单个位的条件码寄存器,它们描述了最近的算术或逻辑操作的属性。我们可以检测这些寄存器来执行条件分支指令。它们分别是:

  • CF:进位标志。最近的操作使最高位产生了进位,可以用来检查无符号操作数的溢出。
  • ZF:零标志。最近的操作得出的结果为 0.
  • SF:符号标志。最近的操作得到的结果为负数。
  • OF:溢出标志。最近的操作导致一个补码溢出。

条件分支:

  • 原始 C 代码:

      int absdiff(int x, int y){
          if (x < y)
              return y - x;
          else
              return x - y;
      }
    
  • 与汇编代码等价的 goto 版本

      int gotodiff(int x, int ){
          int result;
          if (x >= y)
              goto x_ge_y;
          result = y - x;
          goto done;
      x_ge_y:
          result = x - y;
      done:
          return result;
      }
    

循环:

  • do-while 循环

    • 通用形式:

        do
            body-statement
            while (test-expr);
      
    • 翻译后的形式:

        loop:
            body-statement
            t = test-expr;
            if (t)
                goto loop;
      
  • while 循环

    • 通用形式:

        while (test-expr)
            body-statement
      
    • 翻译后的形式:

        t = test-expr
        if (!t)
            goto done;
        loop:
            body-statement
            t = test-expr;
            if (t)
                goto loop;
        done:
      
  • for 循环

    • 通用形式:

        for (init-expr; test-expr; update-expr)
            body-statement
      
    • 翻译后的形式:

        init-expr;
        t = test-expr;
        if (!t)
            goto done;
        loop:
            body-statement
            update-expr;
            t = test-expr;
            if (t)
                goto loop;
        done:
      

switch 语句

GCC 根据开关情况的数量和开关情况值的稀少程度来翻译开关语句。当开关数量比较多,并且值的范围跨度比较小时,就会使用跳转表

###过程

一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。

栈帧结构:

栈帧的最顶端以两个指针界定,寄存器 %ebp 为帧指针,而寄存器 %esp 为栈指针。

假设过程 P 调用过程 Q,则 Q 的参数放在 P 的栈帧中。另外,当 P 调用 Q 时, P 中的返回地址被压入栈中,形成 p 的栈帧的末尾。返回地址就是当程序从 Q 返回时应该继续执行的地方。

###数组分配和访问

IA32 的存储器引用指令可以用来简化数组访问。例如,假设 E 是一个 int 型的数组,并且我们想计算 E[i],在此, E 的地址存放再 %edx 中,而 i 存放再 %ecx 中,那么,指令为:

movl (%edx, %ecx,4), %eax

这样会地址会执行 Xe + 4i,允许的缩放因子1、2、4、8覆盖了所有基本简单数据类型的大小。