EchoCow

念念不忘,必有回响

念念不忘,必有回响
  menu
99 文章
101 评论
111933 浏览
3 当前访客
ღゝ◡╹)ノ❤️

[阅读] CSAPP 读书笔记 —— 程序的机器级表示(一)

这一章主要的重点就是 汇编语言,通过将 C 语言代码转变为汇编语言让我们熟悉汇编语言的使用。

通常情况下,使用现在的优化编译器产生的代码至少与一个熟练得汇编语言程序员手工编写得代码一样有效。最大的优点是,用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的。

我们学习汇编代码,最重要的是能够理解编译器优化能力,能够分析出来代码中隐含的低效率和错误。通过编译和检查汇编代码,可以知道什么样的代码能股带来什么样的效果。

x86

本章所有的内容都是基于 x86-64,intel 的处理器系列称为 x86,他经历了一个长期的、不断进化的发展过程。以下是一列 intel 处理器的模型。

名称年份晶体管备注
8086197829K第一代单芯片,16 位微处理器之一
8088197929K8086 基础上加了一个 8 位外部总线,最初IBM个人计算机心脏
8087198045K与一个 8086 或 8088 处理器一同运行,执行浮点指令,建立了 x86 系列的浮点模型
802861982134KMS Windows 最初使用平台
i3861985275K体系结构扩展到 32 位,增加平坦寻址模式,Intel 系列中第一台全面支持 Unix 操作系统的机器
i48619891.2M浮点单元集成到了处理器芯片上
Pentium19933.1M对指令集进行小的扩展
PentiumPro19955.5M全新处理器设计,指令集添加“条件传送”指令
Pentium/MMX19974.5M在 Pentium 处理器中增加一类新的处理整数向量的指令
Pentium II19977MP6 微体系结构的延伸
Pentium III19998.2M引入 SSE
Pentium 4200042MSSE 扩展到了 SSE2,增加新的数据类型,以及针对这些格式的 144 条新指令
Pentium 4E2004125M增加超线程,可以在一个处理器上同时运行两个程序;增加啦 EM64T,对 AMD 提出的 IA32 的 64 位扩展的实现,称之为 x86-64
Core 22006291M回归到类似 P6 的微体系结构。Intel 的第一个多核微处理器,但不支持超线程
Core i7, Nehalem2008781M即支持超线程,也有多核,最初的版本支持每个核上执行两个程序,每个芯片上最多四个核
Core i7, Sandy Bridge20111.17G引入 AVX,这是对 SSE 的扩展,支持把数据封装进 256 位的向量
Core i7, Haswell20131.4GAVX 扩展到 AVX2
Core i7, Skylake2015-14纳米制程制造,更新微处理器架构

值得注意得是,自 Core i7, Haswell 之后的,都是自己查询的。同时,2013年的Haswell是英特尔最后一次透露普通cpu的晶体管数目: The Haswell Review: Intel Core i7-4770K & i5-4670K Tested。2014 年年底英特尔却不再公布普通桌面和移动 cpu 的晶体管数目了。

**每个后继的处理器都是后向兼容的。**最初的 8086 提供得内存模型和它的 80286 中的扩展,到 i386 的时候就都已经过时了。原来的 x87 浮点指令到引入 SSE2 以后就过时了。

程序编码

在学习本章的时候,我们需要使用到得是 GCC 编译器,这是 linux 上默认的编译器,现在我们来对一个程序进行一个实验,并讲述它的一个过程:

#include<stdio.h>
int main(){
  printf("Hello world!");
  return 0;
}

通过 gcc -Og -o hello hello.c 指令进行编译,得到一个 hello 的可执行文件。再通过 ./hello 运行即可。

-Og 告诉编译器使用会生成符合原始 C 代码整体结构的机器代码优化等级。

实际上, gcc 命令调用了一整套程序,将源代码转化为可执行程序。

  1. C 预处理器 扩展源代码,插入所有用 #include 命令指定的文件,并扩展所有用 #define 声明的宏。
  2. 编译器 产生一个源文件(hello.s)的汇编代码
  3. 汇编器 将汇编代码转化成二进制 目标代码文件 hello.o
  4. 链接器 将目标文件与实现库函数得代码合并,并产生最终的可执行代码文件 hello

这就是 GCC 的编译过程。

机器级代码

两种抽象:

  1. 指令集体系结构或指令集架构 ISA,定义机器级程序的格式和行为
  2. 机器级程序使用的内存地址是 虚拟地址,提供的内存模型看上去是一个非常大的字节数组,

在编译过程中,编译器把用 C 语言提供的相对比较抽象的执行模型转化为非常基本的指令。汇编代码非常接近机器代码,与机器二进制格式相比,汇编代码得主要特点是他用可读性更好的文本格式表示。

x86-64 的机器代码和原始 C 语言差别非常大,一些通常对 C 语言程序员隐藏的处理器状态都是可见的。

  • 程序计数器(PC,%rip):将要执行的下一条指令在内存中的地址。
  • 整数寄存器文件:包含 16 个命名位置,分别存储 64 位的值。
  • 条件码寄存器:保存最近执行的算术或逻辑指令的状态信息。
  • 向量寄存器:存放一个或多个整数或浮点类型。

代码示例

我们准备一个源代码 mstore.c

long mult2(long, long);

void multstore(long x, long y, long *dest) {
  long t = mult2(x, y);
  *dest = t;
}

通过 -S 选项,生成汇编代码文件

linux > gcc -Og -S mstore.c

生成 mstore.s,如下:

        .file   "mstore.c"
        .text
        .globl  multstore
        .type   multstore, @function
multstore:
.LFB0:
        .cfi_startproc
        pushq   %rbx
        .cfi_def_cfa_offset 16
        .cfi_offset 3, -16
        movq    %rdx, %rbx
        call    mult2@PLT
        movq    %rax, (%rbx)
        popq    %rbx
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE0:
        .size   multstore, .-multstore
        .ident  "GCC: (GNU) 9.1.0"
        .section        .note.GNU-stack,"",@progbits

去掉伪指令(所有以 . 开头的行)

multstore:
	pushq   %rbx
	movq    %rdx, %rbx
	call    mult2@PLT
	movq    %rax, (%rbx)
	popq    %rbx
	ret

通过使用 -c 选项,GCC 会编译并汇编该代码

linux > gcc -Og -c mstore.c

生成一个目标代码文件 mstore.o,他是二进制格式,无法直接查看。

与书上大小不同,书上为 1368 字节,这里 1.4K 字节,猜测应该是 GCC 编译器把编译器的信息也编译了进去

-rw-r--r-- 1 echo echo 1.4K 10月  5 21:58  mstore.o

通过使用 OBJDUMP 进行反汇编

➜ objdump -d mstore.o 

mstore.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <multstore>:
   0:   53                      push   %rbx
   1:   48 89 d3                mov    %rdx,%rbx
   4:   e8 00 00 00 00          callq  9 <multstore+0x9>
   9:   48 89 03                mov    %rax,(%rbx)
   c:   5b                      pop    %rbx
   d:   c3                      retq   

左边每组都是一条指令,右边是等价得汇编语言。一些反汇编的特向需要注意:

  • x86-64 指令长度 1-15 个字节不等
  • 设计指令格式得方式是,从某个给定位置开始,可以将字节唯一解码成机器指令。
  • 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码
  • 反汇编器使用的指令命名规则与 GCC 生成的汇编代码有些细微的差距。

汇编代码两种规范:ATT 和 Intel,我们使用的是 ATT

可以通过 gcc -Og -S -masm=intel mstore.c 得到 Intel 汇编格式代码

如下:

multstore:
	push    rbx
    mov     rbx, rdx
    call    mult2@PLT
    mov     QWORD PTR [rbx], rax
    pop     rbx
    ret

他们有如下不同:

  1. Intel 代码忽略了指令大小得后缀,如 push 而不是 pushq
  2. Intel 代码省略了寄存器名字前面得 % 符号
  3. Intel 代码用不同的方式来描述内存中的位置,例如是’QWORD PTR [rbx]‘ 而不是 ’(%rbx)‘
  4. 在带有多个操作数的指令的情况下,列出操作数的顺序相反。

数据格式

由于从 16 位体系结构扩展到 32 位得,所以 16 位数据类型用 “字” 表示

C 声明Intel 数据类型汇编代码后缀大小(字节)
char字节b1
shortw2
int双字l4
long四字q8
char *四字q8
float单精度s4
double双精度l8

访问信息

一个 x86-64 的 CPU 包含一组 16 个存储 64 位值的通用目的寄存器,这些寄存器用来存储整数数据和指针。

寄存器

操作数

操作数:指出一个操作中要使用的源数据值,以及要放置结果的目的位置。分为三种

  1. 立即数:表示常数值。ATT中,以’$‘后面跟一个用标准C表示法表示的整数
  2. 寄存器:表示某个寄存器的内容
  3. 内存引用:根据计算出来的地址(通常为有效地址)访问某个内存位置

寻址模式,常见表示 Imm(rb, ri, s),组成如下

  • 一个立即数偏移量 Imm
  • 一个基址寄存器 rb
  • 一个变址寄存器 ri
  • 一个比例因子 s(必须为1,2,4,8)

基址和变址寄存器都必须是 64 位寄存器,有效地址计算为 :

$Imm+R[r_b]+R[r_i] \cdot s$

寻址模式

数据传送指令

MOV —— 把数据从原位置复制到目的地址,不做任何变化。

指令效果描述
MOV S, DD ←S传送
movb
movw
movl
movq
movabsq




R←I
传送字节
传送字
传送双字
传送四字
传送绝对的四字
  • 源操作数:立即数
  • 目的操作数:寄存器/内存地址

传送指令的两个操作数不能都指向内存位置。将一个值一个从一个内存地址复制到另一个内存地址需要两条指令

  1. 加载到寄存器
  2. 将该寄存器值写入目的地址

栈数据

后进先出

  • 压入 pushq,等价于

    sub		$8, %rsp		# 栈指针减 8
    movq	%rbp, (%rsp)	# 写到新的栈顶地址
    
  • 弹出 popq,等价于

    movq	(%rsp), %rax	# 读出数据
    addq	$8, %rsp		# 栈指针加 8
    

无论如何,%rsp 指向的地址总是栈顶

念念不忘,必有回响。

如果觉得文章不错或者帮到了您,帮忙点点下面广告呗~谢谢啦~

评论