x86汇编学习笔记

x86汇编学习笔记

cbw:把AL扩充成AX,扩充时要考虑负数

cwd:把AX扩充成DX:AX,扩充时要考虑负数

cdq:把EAX扩充成EDX:EAX,扩充时要考虑负数

一般用于放大被除数,为之后的除法做准备



movsx:符号扩充

movzx:零扩充

1
2
movsx ax, al ;将al符号扩充成ax
movzx ax, al ;将al零扩充成ax



rol: rotate left 循环左移

把16位整数转化成16进制格式输出

1
2
3
mov ah, 9Ah
rol ah, 1
; 9A = 1001 1010 -> 0011 0101



通过下列方式可以使用32位寄存器

1
2
3
4
5
6
.386
data degment use16
data ends

code segment use16
code ends



段首地址(5位16进制数)必须以0结尾



0000:0000~9000:FFFF dos操作系统及用户代码占用的内存空间,总共640K内存空间

A000:0000~F000:FFFF 保留给显卡及ROM



显卡:text mode 与 graphic mode

text mode:80*25



assume ds:data,编译器会把data替换成ds:

若写成assume es:data,编译器则会把data替换成es:

同一个段与多个寄存器有关联时:ds > ss > es > cs

如,若写成assume ds:data, es:data,则会将data替换成ds:



cs:ip和ss:sp会被操作系统初始化

四个段寄存器中只有cs无法通过mov来改变

ds和es也会被操作系统初始化,ds=es=首段地址-10h

首段地址-10h:0000指向一块长度为100h字节的内存块,称为PSP(program segment prefix),PSP是操作系统自动分配给正在运行的程序的,里面存放了命令行参数等信息



图形模式编程

用int 10h把显卡切换到图形模式

如若要切换到分辨率320*200,颜色为256色的图形模式:

mov ah, 0h

mov al, 13h

int 10h

例如:

mov ax, 0a000h

mov es, ax

mov bx, 0

mov byte ptr es:[bx], 4

一个点只需要填一个字节

(x, y)对应的显卡偏移地址=y*320+x,段地址=A000h


mov ah, 0

int 16h

能读取上下左右方向键,并且读取键盘时不回显

mov ah, 1

int 16h

检测键盘缓冲区中有没有曾经按下的键,如果有则zf=0,没有则zf=1

jz nokey

mov ah, 0

int 16h

nokey:

继续刷新游戏画面



堆栈段定义

stk segment stack

db 200h dup(0)

stk ends

加上ss:stk

当源代码中并没有定义堆栈段时,编译器会自动生成一个堆栈段,ss=首段的段地址,sp=0



FLAG寄存器

FL共16位,但只用其中9位,包括6个状态标志和3个控制标志

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

X X X X OF DF IF TF SF ZF X AF X PF X CF

0 0 0 0 0 0 1

mov指令不会影响标志位的状态

  • CF:进位标志

    移位指令也会影响CF的值,最后移出去的那一位会自动保存到CF中

    • jc 有进位则跳
    • jnc 无进位则跳
    • adc 带进位加
    • clc CF=0
    • stc CF=1
  • ZF:零标志

    结果为0时ZF=1

    jz==je jnz==jne

    cmp ax, ax

    jz或je next会跳转到next

  • SF:运算结果的最高位

    mov ah, 7fh

    add ah, 1 ;AH=80h=10000000B, SF=1

    sub ah, 1 l;AH=7F=01111111B, SF=0

    js与jns

  • OF:溢出标志

    在补码规则下,正数加正数得到负数时,OF=1;或者负数加负数得到正数时,OF=1;减法同理

    jo与jno

  • PF:奇偶标志位

    统计第八位中1的个数,偶数为1,奇数为0

    jp/jpe与jnp/jpo

  • AF:辅助进位标志

    第四位向高四位产生进位或借位

    AF跟BCD码有关

    1
    2
    3
    mov al, 29h 
    add al, 8
    daa ;加法的十进制调整,这条指令会根据AF=1做AL=AL+6运算,使得AL=37h
    1
    2
    3
    mov al, 29h
    add al, 1
    daa ;AL=AL+6=2AH+6=30h
  • DF:方向标志

    控制字符串运算的方向

    当DF=0时为正方向,当DF=1时是反方向

    cld:使DF=0

    std:使DF=1

    源首地址<目标首地址时,复制按反方向

    源首地址>目标首地址时,复制按正方向

    只有源地址快和目标地址快有部分重叠时,才需要注意复制的正方向与反方向

  • IF:中断标志

    IF=1时,允许硬件中断;IF=0时,禁止硬件中断

    cli:使IF=0

    sti:使IF=1

    软件中断:在代码中用int n形式来调用某个函数集中的子函数

    硬件中断:有硬件的某个事件触发,并由CPU自动插入并调用一个隐式的int n指令来调用某个中断服务子函数

  • TF:跟踪/陷阱标志

    当TF=1时,CPU会进入单步模式,CPU在每执行一条指令后,会自动在该条指令与下条指令之间插入一条int 1h指令并执行它

    当某条指令执行前TF=1,则该条指令执行后会自动执行int 1h单步中断

    1
    2
    3
    4
    5
    6
    pushf ;push FL
    pop ax; AX=FL
    or ax, 100000000B; 或100h
    ; and ax, not 100h; 或0FEFFh,把AX的第8位清零
    push ax
    popf ;TF=1
  • jg,jl,jge,jle是符号数比较相关的跳转指令

    • jg:jump if greater
      • SF == OF 且 ZF == 0
    • jge:SF == OF
    • jl:SF != OF 不需要考虑ZF的状态
    • jle:SF != OF || (SF == OF && ZF == 1)
  • jcxz:当cx=0时跳转



端口

端口地址范围:[0000h, 0FFFFh],共65536个端口

对端口操作使用指令in和out实现

通过60h端口,CPU与键盘之间可以建立通讯

1
2
3
4
5
6
7
8
in al, 60h ;端口号<=FFh时可之间用常数
或者
mov dx, 60h
in al, dx

out <=FFh的常数, al
或者
out dx, al



32位间接寻址方式

  1. 32位比16位多了以下这种寻址方式:

    [寄存器+寄存器*n+常数]

    其中n=2、4、8

    如mov eax, [ebx+esi*4]

  2. 16位中只有4个寄存器可以用来放在[]内:[bx] [bp] [si] [di]

    32位中对[]中的两个寄存器几乎不加限制



xchg

用于交换两个寄存器之间的值

或者交换寄存器和地址之间的值

1
2
3
4
mov ax, 1 
mov bx, 2
xchg ax, bx ;则ax=2, bx=1
xchg ax, ds[bx]



乘法指令:mul

  • 8位乘法:被乘数为AL,乘积为AX

    • mul bh表示AX=AL*BH
    • mul后面所跟的操作数必须是8位寄存器或8位变量,不能是常数
  • 16位乘法:被乘数为AX,乘积为DX:AX

    • mul bx表示DX:AX=AX*BX
  • 32位乘法:被乘数为EAX,乘积为EDX:EAX

    • mul ebx表示EDX:EAX=EAX*EBX
  • 带符号乘法为imul

    • 第一类用法跟mul指令一样
  • mul及imul的第二类用法可以包含2个或3个操作数

    1
    2
    ①mul eax, ebx ; eax=eax*ebx
    ②imul eax, ebx, 3 ;eax=ebx*3
    • ①②中的第2个操作数可以是寄存器也可以是变量
    • ②中的第3个操作数只能是常数



除法指令:div

  • 16位除以8位得8位
    • ax为被除数,al为商,ah为余数
    • ax / 除数 = AL…AH
  • 32位除以16位得16位
    • dx:ax / 除数 = ax…dx
  • 64位除以32位得32位

    • edx:eax / 除数 = eax…edx
  • idiv:带符号除法



地址传送指令:lea lds les

  • lea 取变量的编译地址

    1
    2
    3
    4
    lea dx, ds[bx+si+3] ;dx=bx+si+3
    mov dx, bx+si+3 ;语法错误!

    lea eax, [eax+eax*4] ;eax=eax*5 用lea做乘法

    []中*后所跟的数只能是2的n次方

  • 远指针

    • 近指针:某个变量的偏移地址
    • 远指针:某个变量的段地址和偏移地址
    • C语言中的指针都为近指针
    • les di, dword ptr ds:[bx] 取出ds:[bx]处的32位,高16位在es,低16位在di
    • lds si, dword ptr ds:[bx] 与les类似,但高16位在ds,低16位在si
    • fword ptr 特指48位宽度的变量



换码指令:在xlat执行前必须让ds:bx指向表,al必须赋值为数组的下标。执行xlat后,al=ds:[bx+al]

inc和dec不改变CF位

adc带进位加法

sbb带借位减法

neg ax ;即求ax=-ax



小数运算

fadd fsub fmul fdiv 小数的运算

1
2
3
pi dd 3.14; 32位小数,相当于float
r dq 3.14159; 64位小数,相当于double
s dt 3.1415926 ;80位小数,相当于long double

CPU内部一共有8个小数寄存器,分别叫做st(0) st(1) st(2) … st(7)

其中st(0)可以简写成st

这八个寄存器的宽度都为80位

在载入数据时,先载入的值比如3.14进入st(0)后,再载入的值比如2.0并不是进入st(1),而是先把st(0)中的3.14存到st(1)中,再将2.0载入st(0)中

fld [a]:将[a]中值载入st中

fstp [a]:将st(0)中的内容取到[a]中,然后将st(0)弹出(st[1]中的内容会移动到st[0]中)

fstp st(0)会将st(0)中的内容清空

每一个小数运算之前都会自动插入一条wait指令

fild [a]:将a中的整数值当作小数载入到st中



逻辑运算指令

AND,OR,XOR,NOT,TEST

test ax, 8000h ;将即做ax&8000h,但不保存结果,只改变标志寄存器的值



移位指令

shl, shr, sal, sar, rol, ror, rcl, rcr

移出去的位都会放到CF中

sal是算术左移

sar为算术右移

shl为逻辑左移

shr为逻辑右移

算术左移(sal)及算术右移(sar)的对象是符号数

逻辑左移(shl)及逻辑右移(shr)的对象是非符号数

rcl:带进位循环左移

rcr:带进位循环右移

1
2
3
4
5
6
7
8
9
10
mov ah, 0b6h 
stc ;CF=1
rcl ah, 1 ;CF=1, ah = 1011 0110 移位前
;CF=1, ah = 0110 1101 移位后
;即把CF和ah当作9个位一起循环转

mov ah, 0b6h
stc ;CF=1
rcr ah, 1 ;AH = 1011 0110, CF=1 移位前
;AH = 1101 1011, CF=0 移位后



字符串复制指令

movsb

rep movsb原理如下:

1
2
3
4
5
6
7
8
again:
if (cx == 0) goto done;
byte ptr es:[di] = byte ptr ds:[si]
if (df == 0) {si ++; di ++;}
else {si --; di --;}
cx --;
goto again
done:

单独的movsb指令所做的操作如下:

1
2
3
byte ptr es:[di] = byte ptr ds:[si]
if (df == 0) {si ++; di ++;}
else {si --; di --;}
movsw

rep movsw原理如下:

1
2
3
4
5
6
7
8
again:
if (cx == 0) goto done;
word ptr es:[di] = word ptr ds:[si]
if (df == 0) {si += 2; di += 2;}
else {si -= 2; di -= 2;}
cx --;
goto again
done:

单独的movsw指令与movsb指令类似

movsd

rep movsd原理如下:

1
2
3
4
5
6
7
8
again:
if (cx == 0) goto done;
dword ptr es:[di] = dword ptr ds:[si]
if (df == 0) {si += 4; di += 4;}
else {si -= 4; di -= 4;}
cx --;
goto again
done:

单独的movsd指令与movsb类似

32位系统

在32位系统中,为ds:esi与es:edi



字符串比较指令:cmpsb, cmpsw, cmpsd

cmpsb:

比较byte ptr ds:[si]与byte ptr es:[di]

当df=0时,si++,di++

当df=1时,si—,di—

rep cmpsb:连续进行比较

repe cmpsb:若本次相等则继续比较下一个

1
2
3
4
5
6
7
again:
if (cx == 0) goto done;
if (df == 0) {si ++; di ++;}
else {si --; di --;}
cx --;
若本次相等,则goto done
done:

repne cmpsb:若本次不想等则继续比较下一个

可以根据zf=1推出两个字符串全等

可以根据zf=0推出两个字符串全不等

cmpsw和cmpsd同理



字符串扫描指令:scasb, scasw, scasd

scasb:

1
2
cmp al, es:[di] 
di++; 当df=1时,为di--

repne scasb:

1
2
3
4
5
6
7
8
next:
if (cx == 0) goto done;
cmp al, es[di]
di++; 当df=1时,为di--
cx--;
je done
goto next
done:

repe scasb:相等则进行下一次扫描



字符串操作指令:stosb, lodsb

stosb:

1
2
es[di] = al 
di ++;

rep stosb:循环cx次stosb

stosw:储存ax

stosd:储存eax


lodsb:

1
2
al = ds:[si]
si ++;

lodsb通常没有rep前缀

lodsw:取出ax

lodsd:取出eax



控制转移指令:jmp, call, int

byte ptr

word ptr

dword ptr

fword ptr

qword ptr

tbyte ptr

短跳指令:机器码由2字节构成

第1个字节=EB,第二个字节=目标地址-下调指令的偏移地址

所有条件跳转都是近跳

  • call 近跳,仅push ip,返回用ret
  • call dword ptr,远跳,push cs, push ip,返回用retf
  • int,远眺,pushf, push cs, push ip,返回用iret



汇编语言中的三种参数传递方式

  1. 寄存器传递

    1
    2
    3
    4
    5
    6
    f:
    add ax, ax
    ret
    main:
    mov ax, 3
    call f
  2. 变量传递

    1
    2
    3
    4
    5
    6
    7
    f:
    mov ax, var
    add ax, ax
    ret
    main:
    mov var, 3
    call f

寄存器传递可以多线程,因为操作系统在进行多线程时会自动保存寄存器的值

变量传递不支持多线程

  1. 构造堆栈结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    f:
    push bp
    mov bp, sp
    mov ax, [bp + 4]
    add ax, ax
    pop bp
    ret
    main:
    mov ax, 3
    push ax
    call f
    back:
    add sp, 2



动态变量的构造

将如下c语言函数转化成汇编:

1
2
3
4
5
int f(int a, int b) {
int c;
c = a + b;
return c;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
f:
push bp
mov bp, sp
sub sp, 2
mov ax, [bp + 4]
add ax, [bp + 6]
mov [bp - 2], ax
mov ax, [bp - 2]
mov sp, bp
pop bp
ret
main:
mov ax, 20
push ax
mov ax, 10
push ax
call f
back:
add sp, 4

C语言函数中需要保护bp, bx, si, di(32位为ebp, ebx, esi, edi):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f: 
push bp
mov bp, sp
sub sp, n ;其中n一个常数,用来为动态变量分配空间
push bx
push si
push di
...
pop di
pop si
pop bx
mov sp, bp
pop bp
ret

32位以上的cpu允许[esp+n]或[esp-n]来引用堆栈中的内容



递归

1
2
3
4
int f(int n) {
if (n == 1) return 1;
return n + f(n - 1);
}

上述C语言递归函数可翻译成以下汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
f:
push bp
mov bp, sp
mov ax, [bp + 4]
cmp ax, 1
je done
dec ax
push ax
call f
there:
add sp, 2
add ax, [bp + 4]
done:
pop bp
ret
main:
mov ax, 3
push ax
call f
here:
add sp, 2



结束程序运行但保留内存块

1
2
3
mov dx, 内存块的长度
mov ah, 31h
int 21h

PSP:在code段之前,100字节,由操作系统自动分配

调用int 21h/ah=3h功能时,需要把PSP的占用的空间长度也计算到当前程序占用的内存块长度中

dx = ((100h + code段的长度) + 0fh ) / 10h

除以10h是因为长度单位为节(1节=10h字节)

加上0fh是考虑到code段长度无法被10h整除

1
2
3
final label byte 
相当于final是一个db类型的变量,但没有分配内存
之后可用offset final表示程序结束位置的偏移地址



int、iret

中断指令格式:int n; 其中n的范围是[0, 0FFh]




Author

Gax

Posted on

2022-07-28

Updated on

2023-10-24

Licensed under

Comments