本文最后更新于 2024-12-17,文章距离上次更新已超30天,内容可能有些老了——

突然通知了汇编语言要做实验?!于是近期在恶狠狠补习汇编语言ing

为了方便自己查阅,对常用的一些指令还有操作做了一个记录

如果有错误的地方,欢迎指正!!!

  • 目前更新到hello world 。 可以根据右边目录查看哦!

  • 感谢曾同学的错误指正!(循环移动部分修改)

debug 模式下常用指令:

 r ;查看寄存器的内容
 r ax ;查看ax寄存器的内容
 d ;查看内存的内容
 d 1000:0000 ;查看1000:0000处的内存
 e ;修改内存的内容
 e 1000 90 ;将1000处的内存改为90
 q ;退出debug
 u ;反汇编
 u 1000 ;从1000处开始反汇编
 t ;单步执行
 g ;执行到结束
 a ;以汇编指令的格式写入一条指令
 a xxxx:xxxx ;在xxxx:xxxx处写入一条指令 
 ;这些命令后面都可以直接加上地址,表示在这个地址上执行
 ​
 在遇到loop 的时候,可以 p 直接跳过循环(并且执行完毕)

寄存器简介

 ah/al : 累加寄存器      ax  (accumulator) 8位/16位
 bh/bl : 基址寄存器      bx  (base) 8位/16位
 ch/cl : 计数寄存器      cx  (counter) 8位/16位
 dh/dl : 数据寄存器      dx  (data) 8位/16位
 sp    : 堆栈指针寄存器  (stack pointer)
 bp    : 基址指针寄存器  (base pointer)
 si    : 源变址寄存器    (source index)
 di    : 目的变址寄存器  (destination index)
 ip    : 指令指针寄存器  (instruction pointer)
 cs    : 代码段寄存器    (code segment)  ; 下面几个都是段寄存器,而上面的是通用寄存器
 ds    : 数据段寄存器    (data segment)
 ss    : 堆栈段寄存器    (stack segment)
 es    : 附加段寄存器    (extra segment)
 ​
 ; 标志寄存器,用于计算结果的标志,一些标志只在计算发生后会发生变化,在mov等指令中不会发生变化 
                                         ;在寄存器中显示的是1或0,但是在指令中显示的是ZR或NZ(debug -r 查看)
 of : overflow flag 溢出标志              NO / OV   (no overflow / overflow) ; 与cf的区别在于,cf是最高位的进位/借位,而of是符号位的进位/借位。比如八位,计算结果在-128~127内,不会ov,但可能cy(ff+ff)
                                                                             ; 符号位变化,ov会变化。而最高位变化,cf会变化
 sf : sign flag 符号标志                  NG / PL   (negative / positive)
 zf : zero flag 零标志                    ZR / NZ   (zero / not zero)
 af : auxiliary carry flag 辅助进位标志
 pf : parity flag 奇偶标志                PE / PO   (parity even / parity odd)
 cf : carry flag 进位标志                 CY / NC   (carry / no carry)
 df : direction flag 方向标志
 tf : trace flag 跟踪标志
 if : interrupt flag 中断标志

常用指令

 ​
 dir ; 查看当前目录下的所有文件
 ​
 mov ax,bx
 add ax,bx
 sub ax,bx
 mul bl  ; 8-bit multiplication, result in ax
 mul bx  ; 16-bit multiplication, result in dx:ax ,被乘数在ax中
 div bl  ; 8-bit division, quotient in al, remainder in ah
 div bx  ; 16-bit division, quotient in ax, remainder in dx
 inc ax  ; ax = ax + 1
 dec ax  ; ax = ax - 1
 ​
 shl ax,1 ; ax = ax << 1 ,低位补0
 shr ax,1 ; ax = ax >> 1 ,高位补0
 ​
 ; 循环移动
 rol ax,1 ; ax = ax << 1, 最高位移到最低位
 ror ax,1 ; ax = ax >> 1, 最低位移到最高位
 ​
 ds ; data segment 当前内存的段地址。后续调用[]时,会自动加上ds
 ;ds 不能直接赋值,只能通过mov赋值
 ; 在 mov ax,1001 中,如果改为mov ax,[1001], 那么会从ds:1001处读取数据,而不是直接赋值1001
 ; 可以mov ax,[bx+1] 从ds:bx+1处读取数据,但是不能mov bx,[ax], 因为ax是段地址,不是偏移地址
 ; 等效mov ax,[bx].1 从ds:bx+1处读取数据,在括号外面则是.而不是+  。 常数在前面直接写,在后面需要再前面加一个.
 ; 或者等效mov ax,1[bx]  /  mov ax,[bx][si]
 ; 只有bx,si,di(偏移地址)是可以放在这个中括号中的,其他寄存器都不能放
 ; 也可以mov ax,[bx+si+1] 从ds:bx+si+1处读取数据,或者加di,但是只能二者选一
 ; 即si,di只能2选一,bp和bx也只能2选一
 ; 上述中的 bx 也可以换为bp。区别是在没有给出段地址的时候,bp默认是ss,而bx默认是ds为段地址
 ; --> 实际上是 mov ax,[ds:bx+1] ,这边默认是ds,会省略。bp则是默认ss:bp
 ​
 cs:ip ; code segment 当前代码的段地址:偏移地址。运行代码是从这里开始的
 ​
 ss:sp ; stack segment 当前栈的段地址:偏移地址。栈指针
 ​
 jmp ; 无条件跳转, 调到指定的ip(段地址:偏移地址)
 ; 如果只写了jmp 0x1234, 那么段地址默认是当前代码段地址
 ​
 ​
 ;对栈进行操作
 push ax ; 将ax的值压入栈
 pop ax ; 将栈顶的值弹出到ax
 ;栈是从高地址向低地址增长的
 ​
 ​
 ;与标志位寄存器有关的操作
 adc ax,1 ; ax = ax + 1 + cf -- 带进位的加法
     ; 模拟 add ax,bx  --> add al,bl  +   adc ah,bh
 sbb ax,1 ; ax = ax - 1 - cf -- 带借位的减法
     ; 模拟 sub ax,bx  --> sub al,bl  +   sbb ah,bh
 ​
 cmp ax,bx  ; 比较两个数,不会改变寄存器的值,只会改变标志位寄存器的值 ,通常配合跳转指令使用
 相应跳转指令: ; 它们会根据标志位寄存器的值来决定是否跳转
     je  ; zf=1              jump if equal
     jne ; zf=0              jump if not equal
     jl  ; sf!=of            jump if less
     jle ; sf!=of or zf=1    jump if less or equal    
     jg  ; sf=of and zf=0    jump if greater
     jge ; sf=of             jump if greater or equal
     jb  ; cf=1              jump if below
     jbe ; cf=1 or zf=1      jump if below or equal
     ja  ; cf=0 and zf=0     jump if above
     jae ; cf=0              jump if above or equal
     jna = jbe = jle        ;jump if not above  ; 这几个都是同样的意思,按自己觉得好记的来就行(
     jnb = jae = jge        ;jump if not below    

编写程序的板子

  • 在真正编写汇编程序的时候,出了上述常用代码外,还需要有一些伪指令

 assume cs:codesg,ds:data,ss:stack    ;assume是一个伪指令,用于暂时指定段寄存器
 ​
 data segment   ;标志下面是数据段
     xxx
 data ends
 ​
 stack segment   ; 标志下面是栈段
     xxx
 stack ends
 ​
 codesg segment ;标志下面是代码段
     xxx
     int 21H  ; 表示中断
 codesg ends
 ​
 end  ;标志着整个程序的结束

运行程序

 1.直接利用dosbox进行编译运行
 -- 将文件写好后保存在masm那个文件夹中,并且将文件后缀名改为.asm
 -- 然后在dosbox中,直接写下masm ,然后根据提示输入你的文件名
 -- 然后输入link,再次根据提示写下你的文件名
 此时生成了一个exe文件。
 -- 然后直接在dosbox中写 刚刚的文件名.exe 就可以运行
 -- 也可以debug 文件名.exe 对刚刚的代码进行debug
 ​
 2.在vscode中,下载好对应的扩展,调整配置为dosbox,masm,然后直接右键写好的文件,就可以运行了。

续常用指令:补充指令

 ​
loop ;循环,需要配合cx计数寄存器使用

mov cx,10 ;cx计数寄存器,循环次数  相当于ax乘以自己10次,变成ax的11次方
s: 
    add ax,ax
    loop s
;注意运行到loop s 这里,是类似--c != 0 的类型,如果cx一开始为0,到了这里cx会变成FFFFH,然后继续循环,直到cx为0
;所以cx不能赋值为0,否则会一直循环下去


call 与 ret
call 用于调用一个过程,ret用于返回到调用call的地方

call 过程名

过程名 proc     ; 这部分写在int 21h 之后。
    ;过程体
    ret

举例:
mov cx,3
call s      ;在这里调用s过程
mov bx,ax
mov ax,0
int 21h     ; 标志这段代码的结束
s proc      ;s定义在int 21h之后。代码段正常进行的部分最后一定要写上21h,否则会死循环。
    add ax,1
    ret     ;返回到调用s的地方


(call far pt) 与 (retf) : 当函数定义在比较远的地(超出了一个段,则需要使用callf与retf)
本质上,call实现的是压栈过程,将call指令下一句指令位置存入栈中,而ret实现的是出栈过程,将返回值作为ip的值。
如果栈中没有合适的值,(如没有存数据,里面全部都是0000的情况),则会由于始终跳转到0000,容易出现死循环。


定义数据:   
db   ;定义一个字节 8位
dw   ;定义一个字 16位
dd   ;定义一个双字 32位
dq   ;定义一个四字 64位

;例如 dw 0a123H  注意,在文件中编写数据时,不能以字母开头,所以这里补上了一个0 ,实际输入的是a123h(h表示是16进制数)
; 注意,dw向下 由于是按字存储,可能会出现高低位颠倒的情况,所以在使用时要注意。一般用db比较多。

; 如果数据直接存在代码段中,会引起混淆。虽然我们可以通过start来区分,,在代码段内部写start:  codesg end 后面写上 end start
; 但不利于维护,所以一般数据还是写在数据段中。
; 需要单独加上数据段和栈段,然后在代码段中引用数据段和栈段。在assume中加上ds:data,ss:stack
; 记得在代码段中使用start,确保从正确的ip开始进行。

定义重复数据:
db 10 dup(0) ;定义10个字节,每个字节的值为0  格式就是 重复次数 dup(数据)

offset 操作符:用于获取变量的偏移地址
例如:
mov ax,offset var1 ;将var1的偏移地址存入ax中
var1: 
    inc bx ; 上面offset后返回的,就是var1这个标识符下面第一句代码所在的地址
    dec nc 

段内近转移
jmp short s ; 跳转到s标号处,short表示跳转的范围是-128到127
jmp near ptr s ; 跳转到s标号处,near ptr表示跳转的范围是-32768到32767
远转移
jmp far ptr s ; 跳转到s标号处,far ptr表示跳转的范围是-2^31到2^31-1

jmp word ptr ds:[0] ; 获取ds段中偏移地址为0的,一个字的数据,然后跳转到这个地址
同理, mov word ptr ds:[0],0 ; 将0存入ds段中偏移地址为0的一个字的数据中 
dword ptr 则是读取2个字

比如后面是一个字节数据,我们需要用两个字节,就在前面加上一个word ptr

数组:
arr db 1,2,3,4,5,6,7,8,9,10 ; 定义一个数组
当我们在数据段定义了这个数据,想要在代码段使用,
需要先将数组段地址存入ds中,然后通过ds:arr来引用数组

数组也可以存字符串:
str1 db 'hello world','$' ; $表示字符串结束符,相当于c语言中\0
mov ax,data
mov ds,ax ;由于不能直接将数据存入ds,所以先用ax转接
mov ax,arr[2] ; 此时就可以直接使用了。
;上述也等价于:
mov si offset arr ;获取arr的相对于ds的偏移地址,存入si
mov ax,ds:[si+2]



一些常用的ASCII码:
空格: 32 (十进制) / 20 (十六进制)
回车: 10 (十进制)

hello world

assume cs:codesg,ds:data,ss:stack
data segment
    str db 'hello world','$' ; $是截止符
data ends
stack segment
    db 10 dup (0)
stack ends
codesg segment
    start:
        mov ax,data ; 将数据段的地址存入ax,然后存入ds
        mov ds,ax
        mov dx,offset str ;获得str的偏移地址,存入dx

        mov ah,9 ;中断 + 输出dx到终端
        int 21h

        mov ah,4ch ;安全退出
        int 21h
codesg ends
end start

斐波那契数列

comment*
斐波那契数列求和
c++中:
for(int i = 0; i < 100; i++){
    fib[i] = fib[i-1] + fib[i-2];
}
*comment

assume cs:codesg,ds:data,ss:stack
data segment
    fib dw 1h,1h,100 dup (0)

data ends
stack segment
    db 100 dup (0)
stack ends

codesg segment
    start:
        mov ax,data
        mov ds,ax

      
        mov bx,4 ; 从第三个数开始,第一个数00,第二个数02,第三个数就是04(因为是dw型数据)
        mov cx,20
        s:
        mov ax,0
        add ax,fib[bx-2] ; fib[i-1] , 由于fib是dw型数据,所以每次要加2
        add ax,fib[bx-4] ; fib[i-2]
        mov fib[bx],ax
        add bx,2
        loop s


        mov ah,4ch
        int 21H

codesg ends
end start