汇编语言重要知识整理
TOC
- TOC
- 8086 PC 机内存地址空间分配
- 段寄存器
- 关于 SS,SP
- 伪指令
- 寻址方式
- Debug
- 实模式和保护模式
- Mul 与 Div
- 有符号和无符号比较
- 标志位
- In 和 Out
- 问题
- 实践: 自旋锁
- 参考
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
ormov ax, VARW
ormov ax, [VARW]
(VARW 是内存字变量) - 直接寻址:
mov ax, [1234H]
- 寄存器直接寻址:
mov ax, bx
- 寄存器间接寻址:
mov ax, [bx]
- 寄存器相对寻址:
mov ax, [bx+1]
- 基址+变址寻址:
mov ax, [bx+si]
ormov 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 ptr
或 byte 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,4c00h
和 int 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,成功实现自旋与锁的释放。
参考
- 维基百科
- 王爽《汇编语言》