这是我阅读《汇编语言:基于 Linux 环境 第 3 版》的笔记
mov 指令
mov 指令将数据从源复制到目的地,一旦复制到目的地后数据不会从源消失,而不是实际的“移动”。
|
|
有两个操作数 eax 和 1。
在汇编中机器指令的第一个操作数是目标操作数,第二个操作数是源操作数。
代码中源操作数(字面常量 1)被复制到目标操作数 eax 中。
|
|
立即数,只有源操作数可以是立即数。目标操作数是数据要取得地方。立即数由字面常量组成。
|
|
运行后寄存器 eax 数据为 0x5a595857。这个值与 WXYZ 等值。因为 x86 是小端字节序,所以从后往前放。
|
|
一系列的寄存器操作。
内存数据
立即数是内置在机器指令本身内部的数据,寄存器数据是存放在某个位于 CPU 内部寄存器的数据,而内存数据是存储在分配给某个程序的系统内存中的某个 32 位内存地址处的数据。
如果想要指定的数据是位于寄存器中的内存地址中的值,而不是寄存器本身中的数据,需要在寄存器名字的两边加上方括号(解引用)。比如想要 EBX 中的内存地址的字移到寄存器 EAX 中,应该使用以下指令:
|
|
还可以在寄存器括号中添加字面常量。比如以下指令:
|
|
这里将内存中特定地址的 32 位数据加载到 eax 寄存器中。其中 [ebx + 16] 中的 +16 是立即数偏移量,表示 ebx 指向的地址基础上向后偏移 16 字节。
甚至可以添加两个通用寄存器甚至是多个叠加:
|
|
混淆数据和它的地址
|
|
在汇编中变量名代表的是地址,而不是数据。这里 mov 指令实际复制的是 hello 的地址。
而如果加上了方括号:
|
|
这时候所作的事从 hello 代表的内存地址中取出前 32 位数据,从最低有效字节开始将其加载到 ecx 中,此时 ecx 将保存 Hell 四个字符.
如果只想要其中一个单独字符,只想要加载到一个字节大小的容器(寄存器)中。可以通过 AL 来寻址 EAX 的最低有效字节:
|
|
除此之外可以用 ax 指代 EAX 中的两个字节
|
|
这里将 hello 指向地址的数据修改为 G,使用 byte 关键字强制指令按字节操作。会忽略空格
CPU 标志位
CPU 在执行算术运算和逻辑运算时,会根据运算结果设置一些状态标志,这些标志存储在 eflag 寄存器中。标志位在程序控制、调试和异常处理中起着至关重要的作用。
EFlags 寄存器中的每一位标志都有一个由两个或三个字幕组成的符号,大多数程序员通过这些符号来了解它们。
- CF 进位标志 (Carry Flag)
用于指示无符号数运算时是否发生了进位或借位。 - PF 奇偶标示 (Parity Flag)
用途: 用于奇偶校验
- 低 8 位结果中 1 的个数是 偶数,PF = 1;
- 低 8 位结果中 1 的个数是 奇数,PF = 0。
- AF 辅助进位标示 (Auxiliary Carry Flag)
主要用于 BCD(十进制编码)运算。 - ZF 零标志 (Zero Flag)
用于指示运算结果是否为零。- 结果为 0,则 ZF = 1;
- 结果不为 0,则 ZF = 0。
- SF 符号标志 (Sign Flag)
用于指示运算结果的符号,它的值等于运算结果的最高位- 结果的最高位为 1(负数),则 SF = 1
- 结果的最高位为 0(正数或零),则 SF = 0
- TF 陷阱标志 (Trap Flag)
- 当 TF = 1 时,CPU 每执行一条指令就会触发一次调试中断。
- 用途: 主要用于单步调试
- IF 中断标志 (Interrupt Flag)
- IF = 1 时,CPU 允许外部 可屏蔽中断(IRQ) 发生。
- IF = 0 时,CPU 屏蔽中断,用于关键代码执行期间。
- DF 方向标志 (Direction Flag)
控制字符串操作的方向- DF = 0 从低地址到高地址(自增模式)
- DF = 1 从高地址到低地址(自减模式)
- 用途: 在 MOV 等字符串指令中决定数据的存取顺序
- OF 溢出标志 (Overflow Flag)
- 当对一个有符号整数量的算术运算操作结果过于庞大,以至于不能适合原来的操作数时,溢出标志 OF 被设置。当进位标示被标记时,溢出标志通常也被设置。
- ID
INC 和 DEC
INC 和 DEC 将一个操作数分别加 1 和减 1。他们均只需要一个操作数。
|
|
dec 将 ebx 的值减 1,上例中从 0x02d 变成 0x02c。
inc 将 eax 的值加 1,上例中从 0x0fffffff 变成 0x10000000
标志跳转
条件转移指令是测试。它们测试某个标志的值,要么继续往下走,要么跳转到程序的其他部分。
最简单的条件转移指令是 JNZ。如果不为零则跳转。
JNZ 指令测试零标志(ZF)的值。如果 ZF 已经被设置(即等于1),则什么也不发生。但是如果 ZF 没有被设置,那么执行到程序中一个新目的地
|
|
这个汇编程序仅在 x86 下可以正常运行调试,64 位会直接段错误。
代码执行结束后 Snippet 下字母全部转换成小写。
|
|
把 ebx 放到方括号内,引用的是 Snippet 的内容。byte 大小说明符告诉 NASM 我们只是将一个字节写到 EBX 中存放的内存地址中。
补码和 NEG
|
|
这段代码不断进行 eax 自减操作。
通过查看 eax 的值,可以看到从最初的 0x5 随着自减到 -1 变成 0xffffffff。
这就是补码。
|
|
neg eax 将 42 变成 -42。 -42 用反码表示。 再将 eax 加上 42 可以发现变成 0 了。
符号拓展和 MOVSX
因为寄存器的最高位作为符号位,但是比如 16 位寄存器的数据迁移到 32 位,此时用 mov 指令只纯粹按位复制,显然这不是我们想要的数据。
|
|
在这里,对 ax 赋值为 -42 但是在 kdbg 中,其对应的寄存器 rax 数据却为 65494。我们应该使用 MOVSX 来进行处理。
隐式操作数和 MUL
在 x86 指令集中,有两套乘法和除法指令。一组是 MUL 和 DIVV,用于处理无符号计算。另一组是 IMUL 和 IDIV,用于处理有符号计算。 MUL 将两个值相乘,然后返回一个结果。
|
|
上述代码后 ral 值变成 1000,也就是 100 * 10。
| 机器指令 | 显式操作数(因子1) | 隐式操作数(因子2) | 隐式操作数(结果) |
|---|---|---|---|
| MUL | r/m8 | AL | AX |
| MUL | r/m16 | AX | DX and AX |
| MUL | r/m32 | EAX | EDX and EAX |
一但结果需要不止 16 位,D 系列寄存器就被征用来存放计算结果的高序位部分。如果将两个 16 位值相乘得到结果为 02A456Fh,那么寄存器 AX 中会包含 0456Fh,而 DX 寄存器中则存放 02Ah。
mul 指令的操作数不能为立即数。
MUL 和进位标志
当乘法结果的值溢出低序位寄存器时,乘法指令将设置进位标志(CF)。经过一次 MUL 指令后,如果发现 CF 被设置为 0,则可以忽略高序位寄存器,如果高序位寄存器中存在有效数字则 CF 为 1。
|
|
这段汇编在执行 mul 操作前在 gdb 下通过 info reg eflags 输出仅有 [IF] 标志符。说明只有中断标识被设为 1。而当执行 mul 后,eflags 的值中为 1 的标志符有: [ CF(进位标志) PF(奇偶标志) SF(符号标志) IF(中断标志) OF(溢出标志) ] 。
这里,CF 主要告诉你的是计算结果的高位部分存在有效数字,而这些有效数字存放在用于 32 位乘法运算的 EDX 寄存器中。
使用 DIV 实现无符号除法
DIV 将一个值和被另外一个值除,然后得出一个商和一个余数,这是整数运算。而要执行小数计算就要使用 x86 的数学处理器,比较复杂。
DIV 指令不影响任何标志。
一些符号
r8 = AL AH BL BH CL CH DL DH
r16 =AX BX CX DX BP SP SI DI
sr =CS DS SS ES FS GS
r32 = EAX EBX ECX EDX EBP ESP ESI EDI
m16=16位内存数据
i8=8位立即数
i32= 32位立即数
d16 =16位有符号位移
m8 =8位内存数据
m32 = 32位内存数据
i16=16位立即数 d8 =8位有符号位移
d32= 32位无符号位移
大小说明符: BYTE WORD DWORD
NEG 求补指令
| 表示 | 正数 | 负数 |
|---|---|---|
| 原码 | 数据本身的二进制表示(符号位为 0) | 数据本身的二进制表示(符号位为 1) |
| 反码 | 和原码一样 | 符号位不变,其余各位按位取反 |
| 补码 | 和原码一样 | 反码加一 |
neg reg/mem 即用 0 减操作数(把操作数按位取反,末位加 1),并将求得的结果存入指定的寄存器或内存单元
|
|
0x64 为 0b0110 0100
按位取反 0b1001 1011
末位加1 0b1001 1100
参考资料
- 汇编语言:基于 Linux 环境 第 3 版 清华大学出版社
- 汇编求补指令neg