第13章 x86汇编器编程

提示:make a compiler 系列


一、基于GNU汇编器的编程

1.GNU汇编器

1.1GNU由来

  GNU是一个操作系统,其内容软件完全以GPL方式发布。这个操作系统是GNU计划的主要目标,名称来自GNU’s Not Unix!的递归缩写。GNU的创始人,理查德·马修·斯托曼(自由软件之父),将GNU视为“达成社会目的技术方法”。

1.2GNU汇编器

  一般来说,UNIX 的 C 编译器将 C 语言代码转换为汇编语言代码,汇编语言再经过汇编器处理,得到目标文件(object file)。之所以用汇编语言作为转换中介,是因为机器语言难以阅读。cbc 沿用 UNIX 的做法,其编译器输出汇编语言的代码。汇编器选用 Linux 上广泛使用的GNU as。GNU as 由 GNU 提供,包含在名为 binutils 的包下。gcc 所使用的汇编器也是 GNU as。

二、GNU汇编器的语法

  GNU as 的代码由指令、汇编伪操作、标签和注释这 4 个要素组成。通常除注释外,每一个要素单独占用一行。

2.1 指令

  指令(instruction)是直接由 CPU 负责处理的命令。行首缩进,并且不以点“.”开始的行都是指令行。下面举几个指令的具体例子。

movl %esp, %ebp
pushl %ecx
subl $16, %esp

  指令由标识命令种类的助记符(mnemonic)和作为参数的操作数(operand)组成。以指令movl %esp, %ebp 为例,movl 为助记符,%esp%ebp 这 2 个是操作数。有多个操作数时以逗号来分割。

2.2汇编伪操作

  以点“.”开头,末尾没有冒号“:”的行都是汇编伪操作(directive)行。例如 .file "hello.c".text.globl main 都是汇编伪操作。下面再举一些汇编伪操作的例子。

 	.string "Hello, World!"
 	.text
.globl main
 	.type main, @function

  汇编伪操作是由汇编器,而非 CPU 负责处理的指令。一般用于在目标文件中记录元数据(meta data)或者设定指令的属性等。例如 .string 是用来定义字符串常量的汇编伪操作,.text 是提示代码段的汇编伪操作。
  因为 .string、.text 和 .globl 行首的缩进不同,所以可能会被误认为是不同类型的语法关键字。这只是 gcc 输出代码的习惯而已,无论是否有行首缩进,都不会影响汇编伪操作的运行结果。

2.3标签

  以冒号“:”结尾的行都是标签行。例如 .LCO:main:。使用标签的例子如下所示。

.LC0:
	 .string "Hello, World!"

  标签具有为汇编伪操作生成的数据或者指令起别名(标上符号)的功能,这样就可以在文件的其他地方调用通过标签定义的符号。例如上述代码就是为 .string 汇编伪指令定义的字符串标上符号 .LC0。
  汇编语言中可用于名字(符号)的字符范围比 C 语言广,字母数字“_”“$”以及“.”都可以使用。

2.4注释

GNU as 可以使用两种注释,即单行注释和块注释。

mov $1, %eax # 将 eax 寄存器置为 1

mov $0, %eax /* 所有内存
			 	所有寄存器
				将所有指令的值置为 0
				然后我也返回 0 */
ret

2.5助记符后缀

  先来说一下指令的助记符后缀(mnemonic suffix)。刚才我们提到了 movl subl 为助记
符。更准确地说,movsub 为助记符,末尾的 l 是后缀。llong 的缩写,表示作为操作
对象的数据的大小。l 是表示数据的大小为 32 位的后缀。
指令后缀

2.6各种操作数

  指令的参数(操作数)有如下 4 种。

  1. 立即数
      立即数(immediate value)是 C 语言中的字面量。机器语言中,立即数以整数的形
    式出现,能够高速访问。像 $27 这样,立即数用 $ 来标识。立即数有 8 位、16 位和 32 位。
      其次,寄存器当然也能作为操作数。GNU 汇编器规定寄存器必须以 % 开头,例如 eax 寄存器写作 %eax。

2.寄存器
3. 直接内存引用
  直接内存引用(direct memory reference)是直接访问固定内存地址的方式。GNU 汇编器会将任何立即数都解释成内存地址并访问。例如,若只写 0 的话,就会访问 0 地址。
  比起立即数,更常用的是使用符号(symbol)直接访问内存。例如 .LC0 的意思是访问符号 .LC0 所指向的地址。符号在汇编和链接的过程中会被置换为立即数(内存地址),因此对于CPU 来说,使用符号和直接编写立即数没有差别。
4. 间接内存引用
  间接内存引用(indirect memory reference)是将寄存器的值解释为内存地址并访问的方式。

2.7间接内存引用

间接内存引用中最复杂、最通用的就是下面这样的形式。dispbaseindexscale中的任何一者都可以省略。如下代码,由易到难有4中访问情况,下面一一阐明。

(%eax)
4(%eax)
(%ebx, %eax, 4)
disp(base, index, scale)
  1. 最简单的间接内存访问:

  即只指定基地址(base)的形式。上述表达式将 eax 寄存器中的数据作为内存地址来访问内存。如果将(C 语言的)变量 var 的地址赋给 %eax,那么 (%eax) 就是变量 var 的值。

  1. 带有 disp 的形式。

  上述形式的间接内存引用是在 %eax 寄存器的数据的基础上加上 disp 的 4,以此作为内存地址进行访问。在 C 语言中,这就相当于访问如下所示的结构体 point 中的成员 y 时的情况。

  1. 使用 index 和 scale 的情况。

  上述形式的间接内存引用所访问的是 %ebx 寄存器的值加上“%eax 寄存器的值 ×4”后得到的地址。这种形式相当于 C 语言中的数组访问。

  1. 将上述所有形式合到一起

  即访问地址为 disp + (base + index * scale) 的内存。base 和 index 为寄存器,disp 为立即数(包括符号),scale 必须是 1、2、4、8 之中的任意立即数。

2.8 x86指令概要

在这里插入图片描述

三、传输指令

本节涉及到的传输指令如下表
在这里插入图片描述


3.1 mov指令

  mov 是在寄存器或内存之间传输数据,或者将立即数加载到寄存器或内存的指令。(这里说的传输,指的是复制,并不是剪切)
  mov 的第 1 操作数表示传输“源”,第 2 操作数表示传输“目标”。例如“mov 内存 , 寄存器”表示将内存中的值加载到寄存器。
在这里插入图片描述


3.2 push 指令和 pop 指令

在这里插入图片描述
push 指令将数据压栈。具体来说,将 esp 寄存器减去压栈的数据的大小,再将数据存储到esp 寄存器所指向的地址。(esp 是stack pointer)
在这里插入图片描述
pop 指令将数据出栈并写入寄存器。具体来说,将数据从 esp 寄存器所指向的地址加载到寄存器,再将 esp 寄存器加上出栈的数据的大小。

在这里插入图片描述


3.3 lea指令

在这里插入图片描述
lea 指令将地址加载到寄存器。lea 是 Load Effective Address(实效地址加载)的简称。“lea 内存 , 寄存器”将内存对应的地址加载到寄存器。

例如下面的 mov 指令表示将ebx 寄存器加 4 后的值作为内存地址进行访问,并将数据加载到 eax 寄存器中。

movl 4(%ebx), %eax

另一方面,将上述语句中的 mov 指令替换为 lea 指令,如下所示。该语句表示将 ebx 寄存器加上 4 后的值保存到 eax。

leal 4(%ebx), %eax

3.4 movsx 指令和 movzx 指令

在这里插入图片描述
movsxmovzx 是将数据从位宽较小的变量扩展为位宽较大的变量的指令。例如在编译 C语言(C♭)的类型转换时就会用到上述指令。

  1. movsx 指令将 8 位或 16 位的数据进行符号扩展并加载到寄存器。
  2. movzx 指令将 8 位或 16位的数据进行零扩展并加载到寄存器。

在这里插入图片描述

3.5 符号扩展和零扩展

这里的扩展指的是将低位(如8位)数据扩展成高位数据(如16位)

  1. 如果需要扩展的数据是无符号数据,则对数据进行零扩展,只需要在数据前面补零,使得数据位数满足要求即可。
  2. 如果需要扩展的数据时有符号的,就需要对数据进行符号扩展
    –原数据的最高位为 0 时在高位补 0(和零扩展相同)
    –原数据的最高位为 1 时在高位补 1

四、算术运算指令

涉及到的算术指令有
在这里插入图片描述


4.1 add指令

在这里插入图片描述
add 指令将第 1 操作数和第 2 操作数相加,并将结果写入第 2 操作数。


4.2 进位标志

  由于寄存器和存储器都有位宽的限制,因此在进行加法运算时就有可能发生溢出。例如 8位的整数 156 和 8 位的整数 100 相加,运算结果的宽度就超过 8 位,发生溢出。
  运算结果发生溢出的话,CPU 的标志寄存器 eflags 中的进位标志(Carry Flag,CF)就会被置位,即被设置为 1。


4.2 sub指令

在这里插入图片描述
  sub 指令用第 2 操作数减去第 1 操作数,并将结果写入第 2 操作数。


4.3 imul 指令

在这里插入图片描述
  imul 指令将第 1 操作数和第 2 操作数相乘,并将结果写入第 2 操作数。
  另外,2 操作数形式的 imul 指令不支持宽度为 8 位的寄存器和存储器作为操作数。只有当2 个操作数都为 16 位或 32 位时才能进行运算。


4.4 idiv 指令和 div 指令

在这里插入图片描述
  idiv 是有符号数的除法运算指令,div 是无符号数的除法运算指令。被除数由 edx 寄存器和 eax 寄存器拼接而成,除数由第 1 操作数指定,计算结果的商和余数分别写入 eax 寄存器和edx 寄存器。运算时被除数、商和余数的数据的位宽是不一样的,详细内容请参考表.
在这里插入图片描述


4.5 inc 指令

在这里插入图片描述
  inc 指令将第 1 操作数加 1,相当于 C 语言中的 ++。


4.6 dec 指令

在这里插入图片描述  dec 指令将第 1 操作数减 1,相当于 C 语言中的 --。


4.7 neg 指令

在这里插入图片描述neg 指令将第 1 操作数的符号进行反转,相当于 C 语言中的一元运算符 -。


五、位运算指令

  所涉及的位运算指令如表所示。
在这里插入图片描述


5.1 and 指令

在这里插入图片描述
  and 指令将第 2 操作数和第 1 操作数进行按位与(bitwise AND 运算,并将结果写入第 2操作数,相当于 C 语言中的 &= 运算符。


5.2 or 指令

在这里插入图片描述
  or 指令将第 2 操作数和第 1 操作数进行按位或(bitwise OR)运算,并将结果写入第 2 操作数,相当于 C 语言中的 |= 运算符。

5.3 xor 指令

在这里插入图片描述
  xor 指令将第 2 操作数和第 1 操作数进行按位异或(bitwise exclusive OR)运算,并将结果写入第 2 操作数,相当于 C 语言中的 ^= 运算符。

5.4 not 指令

在这里插入图片描述
  not 指令将第 1 操作数按位取反(bitwise NOT),并将结果写入第 1 操作数,相当于 C 语言中的“~”运算符。

5.5 sal 指令

在这里插入图片描述
  sal 指令将第 2 操作数按照第 1 操作数指定的位数进行左移操作,并将结果写入第 2 操作数。移位之后空出的低位补 0。相当于 C 语言中的 <<= 运算符。

5.6 sar 指令

在这里插入图片描述
  sar 指令将第 2 操作数按照第 1 操作数指定的位数进行右移操作,并将结果写入第 2 操作数。移位之后空出的高位进行符号扩展。相当于 C 语言中的 >>= 运算符。

5.7 shr 指令

在这里插入图片描述
  shr 指令将第 2 操作数按照第 1 操作数指定的位数进行右移操作,并将结果写入第 2 操作数。移位之后空出的高位进行零扩展。相当于 C 语言中对无符号数进行操作的 >>= 运算符。

六、流程的控制

讲解实现 if 或 while 这样的流程控制语句时所使用的指令,如下表所示。
在这里插入图片描述

6.1 jmp 指令

在这里插入图片描述
  jmp 指令将程序无条件地跳转到第 1 操作数指定的位置。最常用的做法是使用由标签定义的符号(symbol),写成“jmp 符号”。

movl $1, %eax 			# then 部分
 		jmp end_if0 	# 跳转到 if 语句的末尾
else0:
 		movl $4, %eax 	# else 部分
end_if0:
 		pushl %eax 		# if 语句后面的语句
 		call printf

  可以将 jmp 视作设置指令指针(eip 寄存器)的指令。例如上述例子中的 jmp end_if0,汇编后的机器语言会将 end_if0 替换为程序代码的地址(数据)。通过将 jmp_end_if0 的地址设置到 eip 寄存器来控制程序的流程。

6.2 条件跳转指令(jz、jnz、je、jne、……)

  条件跳转指令很多,这里只以jnz指令为例
在这里插入图片描述
  jnz 指令是 Jump if Not Zero 的简称,因此仅当标志寄存器 eflags 中的 ZF(Zero Flag)为 0 时才实施跳转。

 	movl y 的地址 , %ecx # 将变量 y 的值加载到 ecx
 	movl x 的地址 , %eax # 将变量 x 的值加载到 eax
 	cmp %ecx, %eax # 比较并设置标志位
 	jnz lab # 不相等的话跳转到 lab
	# 其他指令
lab:
 	# 其他指令

  cmp 指令是比较两个数据并设置 eflags 的各个标志位的指令。在 x 和 y 的值不相等的情况下,如果调用 cmp 指令,ZF 就会被置 0,从而执行跳转。
  反之,在 x 和 y 相等的情况下,如果调用 cmp 指令,ZF 就会被置 1,跳转不会被执行,直接执行 jnz 后面的指令。

还有其他的条件跳转指令和对应的zflag标志如下表所示
在这里插入图片描述
在这里插入图片描述
  仅看跳转条件的标志位可能无法理解指令的意图,请结合指令的名称来理解。例如 ja(Jump if Above),可以想象它表示“当 cmp 的第 2 操作数比第 1 操作数大(above)时跳转”。表示数据的大小时有 above/below 和 greater/less 这 2 套方式,above/below 用于无符号数的比较并跳转,greater/less 用于有符号数的比较并跳转。
  另外,像 jzjejnzjne 等,这些只是完全相同的指令的不同名称。根据使用场景选用适当的指令名称即可。
  ZFCF 等标志位可以通过下面讲解的 cmp 指令或 test 指令进行设置。

6.3 cmp 指令

在这里插入图片描述
  cmp 指令通过比较第 2 操作数减去第 1 操作数的差,根据结果设置标志寄存器 eflags 中的标志位。cmp 指令本质上和 sub 指令相同,只是 cmp 指令不会改变操作数的值

  操作数和所设置的标志位之间的关系如下表 所示。
在这里插入图片描述

6.4 test 指令

在这里插入图片描述
  test 指令通过比较第 1 操作数和第 2 操作数的逻辑与(bitwise AND),根据结果设置标志寄存器 eflags 中的标志位。test 指令本质上和and 指令相同,只是 test 指令不会改变操作数的值
  test 指令执行后 CFOF 通常会被清 0,并根据运算结果设置 ZFSF。运算结果为 0时 ZF 被置为 1,SF 和最高位的值相同。
  test 指令可用于检查特定的位是否被置位等。以 C 语言为例,在检查 if(flags & EOF_FLAG) 这样的条件时,就可以使用 test 指令。

6.5 标志位获取指令(SETcc)

  获取标志位的指令有很多,这里仅以 sete 指令为例,介绍一下其使用方法,其他的标志位获取指令的用法和 sete 完全相同。
在这里插入图片描述

6.6 call 指令

在这里插入图片描述
  call 指令会调用由第 1 操作数指定的函数。最常见的做法是利用符号将代码写成 call printfcall f 的形式。还可以使用寄存器中设置的函数指针,通过 call *%eax 进行函数调用。

  使用 call 指令的例子如下所示。

call printf # 调用通过符号(立即数)定义的 printf 函数
call *%eax # 利用 eax 中设置的函数指针进行函数调用

具体来说,call 指令可以分解为以下 2 个指令。

		pushl $next_insn
 		jmp 第 1 操作数
next_insn:

也就是说,将 call 指令的下一条指令的地址压栈,再跳转到第 1 操作数指定的地址。这样函数就能通过跳转到栈上的地址从子函数返回。

6.7 ret 指令

在这里插入图片描述
  ret 指令用于从子函数返回。x86 架构的 Linux 中是将函数的返回值设置到 eax 寄存器并返回的。
使用 ret 指令的例子如下所示。

ret # 从子函数返回

ret 指令等价于下面这样的处理。

 	popl %eip

  也就是说,将先前 call 指令压栈的“call 指令下一条指令的地址”弹出栈,并设置到指令指针中。这样程序就能正确地返回到调用子函数的地方。

总结

下一章跟大家一起学习函数与变量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值