初探编译与指令集

127 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情

写在前面

在前面的三讲中,我们从例子、优化方法、位运算等角度认识了软件的性能优化,接下来我们向底层探索从编译到硬件的一系列内容。本文将简单的介绍编译和指令集相关的内容。

编译

学过编译原理的同学应该了解,程序从源代码编译到可执行文件大概需要经过预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)和链接(Linking)四个步骤。如下图所示:

image-20230204200025161

预处理会进行一些简单的代码替换等操作,比如将#define的内容进行替换,在这里我们不去讲述。

编译会将经过预处理过的代码进行编译,编译成汇编语言:

image-20230204195745032

汇编语言是便于人去阅读而产生的,实际上机器并不能读懂汇编语言,所以我们需要将其转换成机器能读懂的二进制:

image-20230204195853826

至此我们完成了单个文件的编译过程,将单个文件代码编译成了二进制文件,但是在项目中,我们往往有多个项目文件需要一起编译,这时候就需要用链接器将代码链接到一起:

image-20230204195105690

关于链接,可以阅读《程序员的自我修养:编译、装载与库》一书。

为什么要学习汇编?
  • 汇编能够告诉我们编译器什么能干,什么不能干;
  • 程序的Bug可能来自于底层,比如当开启过高级别的编译器优化时产生的Bug;此外,编译器也可能会有Bug。
  • 有时候我们不得不直接进行汇编级别的代码修改;
  • 逆向工程:当代码携带了debug信息后,我们可以用objdump进行反汇编。

x86-64指令集

x86-64指令集的内容非常的多,本章将从Registers、Instructions、Data types和Memory addressing modes四个方面进行简单的介绍。

Registers

如果了解过计算机组成原理的同学应该知道,寄存器是最快的存储硬件,其相应的价格越昂贵,因此寄存器的数量往往是有限的:

image-20230204201541759

虽然寄存器的数量众多,我们只需要了解几个通用的寄存器即可:

image-20230204201751108

通用寄存器(General-purpose registers)就是最普通的用来存储数据的寄存器,所以其数量比较多,其命名中会描述其宽度信息:

image-20230204202153840 image-20230204202213330

Flag寄存器是记录一些状态信息的,例如是否进位或借位(Carry)、是否为零等:

image-20230204201956726

XMM和YMM寄存器是位宽非常大的寄存器,往往用来做高精度运算或者向量运算。

Data types

x86-64指令集有多个数据类型,这些数据类型有多个位宽:

image-20230204215429085

这些位宽会被用在指令中作为后缀来标识。

Instructions

指令就是告诉机器要做什么的东西,例如add就是告诉CPU要做一个加的指令,CPU在接受到这个信息后会进行解析,并告诉各个模块如何操作。

x86-64的指令格式一般采取<opcode><operand_list>的方式,例如addl %edi, %ecx。针对该汇编语言,会有不同的翻译:我们知道加法是两个操作数,那么结果该放哪个寄存器中呢?在AT&T Syntax中,结果会放在第一个寄存器中;而在Intel Syntax中,结果会放在第二个寄存器中。

x86-64指令主要可以分为数据移动(存取)操作、算数逻辑运算和条件跳转运算:

image-20230204204202405

Data Types会被用在指令后面作为位宽标识:

movq -16(%rbp), %rax

这里的q的意思是quad word,也即64位整数。

此外,指令也可以加后缀来标志是零拓展还是符号拓展,如下所示:

movslq %eax,%rdx //把一个32位的整数放到一个64位的整数里
Memory addressing modes

x86-64支持多种访存方式,分为直接访问和间接访问,其中直接访问有:

  • Immediate:立即数寻址,基于该立即数进行寻址;
  • Register:寄存器寻址,通过寄存器内的数据进行访问;
  • Direct:使用特定地址的数据作为寻址的目标:movq 0x172, %rdi

间接访问有:

  • Register indirect:直接将寄存器中的数据作为地址进行寻址:movq %rax, %rdi
  • Register indexed:基于寄存器中的数据做常数偏移寻址:movq 172(%rax), %rdi
  • Instruction-pointer relative:基于指令寄存器做常数偏移寻址,主要用来做跳转指令寻址:movq 172(%rip), %rdi
  • base indexed scale displacement:最常用的寻址方式,如下图所示:
image-20230204224635385

每次基于该方式寻址时,其计算地址公式为:

Address=Base+indexscale+DisplacementAddress = Base + index*scale + Displacement

后记

无论是编译还是x86-64指令集都不是一篇小小的文章能够介绍玩的,笔者推荐以下的一些内容可供读者参考:

  • 《编译原理》(龙书)
  • 《汇编语言》 网上
  • 《程序员的自我修养:链接、装载与库》

如果对x86-64感兴趣,也可以阅读Intel官方的文档。