汇编语言重要知识整理

15 minute

TOC

8086 PC 机内存地址空间分配

  • 00000~9FFFF: 主存储器地址空间(RAM)
  • A0000~BFFFF: 显存地址空间
  • C0000~FFFFF: 各类ROM地址空间

段寄存器

8086CPU 不支持将数据直接送入段寄存器的操作,ds 是一个段寄存器,所以 mov ds,1000H 这条指令是非法的。

要将 1000H 送入 ds,只好用一个寄存器来进行中转,即先将 1000H 送入一个一般的寄存器,如 bx,再将 bx 中的内容送入 ds。

关于 SS,SP

8086CPU 中,有两个寄存器,段寄存器 SS 和寄存器 SP,栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中。

任意时刻,SS:SP 指向栈顶元素。push 指令和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。

伪指令

在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。

汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终为 CPU 所执行。

而伪指令没有对应的机器指令,最终不被 CPU 所执行。那么谁来执行伪指令呢?

伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

寻址方式

  • 立即寻址:mov ax, 1234H or mov ax, VARW or mov ax, [VARW] (VARW 是内存字变量)
  • 直接寻址:mov ax, [1234H]
  • 寄存器直接寻址:mov ax, bx
  • 寄存器间接寻址:mov ax, [bx]
  • 寄存器相对寻址:mov ax, [bx+1]
  • 基址+变址寻址:mov ax, [bx+si] or mov ax, [bx][si]
  • 基址+变址+相对寻址:mov ax, [bx+si+1]

注:在汇编源程序中,数据不能以字母开头,所以要在前面加 0。比如,9138h 在汇编源程序中可以直接写为 9138h,而 A000h 在汇编源程序中要写为 0A000h。

Debug

  • R命令:查看、改变 CPU 寄存器的内容;
  • D命令:查看内存中的内容;
  • E命令:改写内存中的内容;
  • U命令:将内存中的机器指令翻译成汇编指令;
  • T命令:执行一条机器指令;
  • A命令:以汇编指令的格式在内存中写入一条机器指令;
  • P命令:可用于快速结束一段LOOP,遇到loop时使用;
  • G命令:可以让指令直接执行到某个地址处,如-g 0016执行到0016处代码。

实模式和保护模式

实模式是 Intel 80286 和之后的 x86 兼容 CPU 操作模式。

实模式的特性是一个 20 位元的区段存储器地址空间(意思为只有1MB的存储器可以被寻址),软件可以直接访问 BIOS 例程以及周边硬件,没有任何硬件等级的存储器保护观念或多任务。

保护模式是一种 80286 系列和之后的 x86 兼容CPU的运行模式。

保护模式有一些新的特性,如存储器保护,标签页系统以及硬件支持的虚拟内存,能够增强多任务处理和系统稳定度。

现今大部分的 x86 操作系统都在保护模式下运行,包含 Linux、FreeBSD、以及微软 Windows 2.0 和之后版本。

在纯 DOS 方式(实模式)下,可以不理会 DOS,直接用汇编语言去操作真实的硬件。

因为运行在 CPU 实模式下的 DOS,没有能力对硬件系统进行全面、严格的管理。

但在 Windows 2000、Unix 这些运行于 CPU 保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。硬件已被这些操作系统利用 CPU 保护模式所提供的功能全面而严格地管理了。

Mul 与 Div

  • mul bl ; al * bl => ax
  • mul bx ; ax * bx => dx(H) ax(L)
  • div bl ; ax / bl => ah(余) al(商)
  • div bx ; dx(H) ax(L) / bx => dx(余) ax(商)

有符号和无符号比较

cmp:

无符号比较:

JA JB JNAE: above | below | equal | not

有符号比较:

JG JL JNGE: great | low | equal | not

如果是立即数的比较,cmp 不会识别是 8 位还是 16 位。如果通过 word ptrbyte ptr 进行指定:

1cmp byte ptr[di], 55h

标志位

  • OF overflow,溢出标志,针对有符号数,1 => 溢出
  • DF direction,方向标志,控制数据串操作指令的步进方向,1 => 递减
  • IF interupt,中断允许标志,1 => 开中断
  • TF trap,陷阱标志,1 => CPU 单步执行指令(程序调试)
  • SF sign,符号标志,最高位为 0/1
  • ZF zero,零标志,1 => 结果为零
  • AF auxiliary carry,辅助进位标志,供 BCD 码使用,1 => D3 位出现进位或借位
  • PF parity,奇偶标志,1 => 有偶个“1”出现
  • CF carry,进位标志,针对无符号数,1 => 进位

In 和 Out

在 in 和 out 指令中,只能使用 ax 或 al 来存放从端口中读入的数据或要发送到端口中的数据。

访问 8 位端口时用al,访问 16 位端口时用 ax。

对 0~255 以内的端口进行读写时:

1in al, 20h ; 从 20h 端口读入一个字节
2out 20h, al ; 往 20h 端口写入一个字节

对 256~65535 的端口进行读写时,端口号放在 dx 中:(超出 8 位的接口地址必须用 dx 提供)

1mov dx, 3f8h ; 将端口号 3f8h 送入dx
2in al, dx ; 从 3f8h 端口读入一个字节
3out dx, al ; 向 3f8h 端口写入一个字节

问题

向内存 0:2000:23F 依次传送 063(3FH):

注意 0:2000:23F 等同于 0020:00020:3f,它们描述的是同一内存单元。

 1assume cs:code
 2code segment
 3    
 4    mov  bx, 20h
 5    mov  ds, bx
 6    mov  bx, 0
 7    mov  cx, 40h
 8  s:mov  [bx], bx
 9    inc  bx
10    loop s
11
12    mov  ax, 4c00h
13    int  21h
14
15code ends
16end

mov ax, 4c00h 之前的指令复制到内存 0:200 处:

CX寄存器在debug调试一个可执行程序时,初始值为该程序的字节尺寸大小,要复制 mov ax, 4c00h 之前的指令,需要减去 mov ax,4c00hint 21h 包含的 5 个字节。而由于程序指令的起始地址由 CS:IP 指定,所以将 ds 赋值为 cs。

 1assume cs:code
 2code segment
 3    mov ax, cs ; 程序指令的起始地址由CS:IP指定
 4    mov ds, ax
 5    mov ax, 0020h
 6    mov es, ax
 7    mov bx, 0
 8    sub cx, 5 ; 减去5个字节 mov ax, 4c00h 和 int 21h
 9  s:mov al, [bx]
10    mov es:[bx], al
11    inc bx
12    loop s
13    
14    mov ax, 4c00h
15    int 21h
16code ends
17end

实践: 自旋锁

自旋锁,顾名思义,即自己不断旋转重复进行的锁,当多个线程访问同一资源时,为实现互斥访问,必须给目标资源加锁,此时只允许一个线程访问,此时其他线程无法访问,并且一直重复请求访问,直到该锁被释放。访问完资源的线程及时释放锁以供其他资源访问。

自旋锁可以通过比较替换算法实现:设锁为1时被占用,为0时空闲。当一个线程请求锁时,即进入请求锁循环“spinlock”,设预期值为 0,修改值为 1,让锁值与预期值比较,若锁值等于预期值,则锁空闲,将锁值置为修改值,退出 spinlock 循环;若锁值不等于预期值,则证明锁被占用,继续 spinlock 循环。

为验证是否成功实现自旋,开启一个释放锁线程,请求锁线程自旋一段时间后,释放锁线程进行锁的释放,即把锁值置为预期值0。此时,请求锁线程成功获得锁并退出 spinlock 循环。

下面使用 x86_64 汇编实现自旋锁。

Intel 语法

 1// 尝试获取锁
 2void lock(long *p) {
 3    long a = 0, c = 1;
 4    printf("try to get lock...\n");
 5    __asm__(
 6        "push rax \n\t"
 7        "push rcx \n\t"
 8        "spin_lock: \n\t"
 9        "mov rcx, %[c] \n\t"
10        "mov rax, %[a] \n\t"
11        // 比较并替换算法,若p==rax==0则获得锁并使p=rcx(==1),若p(==1)!=rax则进入自旋。
12        "lock cmpxchg %[p], rcx \n\t"
13        "jne spin_lock \n\t"
14        "pop rcx \n\t"
15        "pop rax \n\t"
16        : [p]"+m"(*p)
17        : [a]"r"(a), [c]"r"(c)
18        : "rcx", "rax"
19    );
20}
21// 释放锁
22void unlock(long *p) {
23    __asm__(
24        "mov %[p], 0; \n\t"
25        : [p]"+m"(*p)
26    );
27}

AT&T 语法

 1void lock(long *p) {
 2    long a = 0, c = 1;
 3    printf("try to get lock...\n");
 4    __asm__(
 5        "pushq %%rax \n\t"
 6        "pushq %%rcx \n\t"
 7        "spin_lock: \n\t"
 8        "movq %1, %%rcx \n\t"
 9        "movq %2, %%rax \n\t"
10        "lock cmpxchg %%rcx, %0 \n\t"
11        "jne spin_lock \n\t"
12        "popq %%rcx \n\t"
13        "popq %%rax \n\t"
14        : "+m"(*p)
15        : "r"(c), "r"(a)
16        : "%rcx", "%rax"
17    );
18}
19void unlock(long *p) {
20    __asm__(
21        "movq $0, %0; \n\t"
22        : "+r"(*p)
23    );
24}

测试自旋锁

初始化锁值为 1,主线程尝试获取锁,进入自旋,子线程在一段时间后释放锁,锁值置为 0,接着,主线程获得锁并把锁置为 1。

 1#include <stdio.h>
 2#include <pthread.h>
 3#include <unistd.h>
 4
 5// intel语法实现自旋锁 > gcc -pthread -masm=intel -o s spinlock.c
 6// 尝试获取锁
 7void lock(long *p) {
 8    long a = 0, c = 1;
 9    printf("try to get lock...\n");
10    __asm__(
11        "push rax \n\t"
12        "push rcx \n\t"
13        "spin_lock: \n\t"
14        "mov rcx, %[c] \n\t"
15        "mov rax, %[a] \n\t"
16        // 比较并替换算法,若p==rax==0则获得锁并使p=rcx(==1),若p(==1)!=rax则进入自旋。
17        "lock cmpxchg %[p], rcx \n\t"
18        "jne spin_lock \n\t"
19        "pop rcx \n\t"
20        "pop rax \n\t"
21        : [p]"+m"(*p)
22        : [a]"r"(a), [c]"r"(c)
23        : "rcx", "rax"
24    );
25}
26// 释放锁
27void unlock(long *p) {
28    __asm__(
29        "mov %[p], 0; \n\t"
30        : [p]"+m"(*p)
31    );
32}
33
34// 释放锁线程
35void *mythread(void* args) {
36    long* p = (long*) args;
37    // 推迟释放锁,此时自旋在进行中
38    sleep(2);
39    // 释放锁
40    unlock(p);
41    printf("after unlock: %ld\n", *p);
42}
43
44int main() {
45    long a = 1; // 设刚开始锁已被获取
46    long *p = &a;
47    // 开启一个用于释放锁的线程
48    pthread_t t1;
49    pthread_create(&t1, NULL, mythread, (void*)p);
50    printf("before lock: %ld\n", *p);
51    // 主线程尝试获取
52    lock(p);
53    pthread_join(t1, NULL);    
54    printf("after lock: %ld\n", *p);
55    return 0;
56}

这里采用 intel 语法编写的自旋锁进行测试,执行命令 gcc -pthread -masm=intel -o s spinlock.c 进行编译。

若采用 AT&T,执行命令 gcc -pthread -o s spinlock.c 进行编译,无需 -masm=intel,因为 gcc 底层默认采用 AT&T。

运行结果

运行结果如下:

1before lock: 1
2try to get lock...
3after unlock: 0
4after lock: 1

一开始锁值为 1,请求锁线程(即主线程)请求获得锁,进入自旋。2s 后释放锁线程进行锁的释放,接着请求锁线程成功获得锁,锁值又被置为 1,成功实现自旋与锁的释放。

参考

  • 维基百科
  • 王爽《汇编语言》