32位基本汇编程序
首先,理解一下简单汇编语言程序
;程序的顶部省略了一些声明
;程序执行两个数相加,并将结果保存在寄存器中
main PROC
mov eax, 5 ;将数字 5 送入 eax 寄存器
add eax, 6 ;eax 寄存器加 6
INVOKE ExitProcess, 0 ;程序结束(调用 Windows 服务(也被称为函数)ExitProcess 停止程序,并将控制权交还给操作系统)
main ENDP ;主程序结束的标记
为了将加法运算的结果保存在变量 sum 中,需要增加一些标记,或声明,用来标识程序的代码和数据区
伪指令:
是嵌入源代码中的命令,由汇编器识别和执行。伪指令不在运行时执行,但是它们可以定义变量、宏和子程序;为内存段分配名称,执行许多其他与汇编器相关的日常任务
.data ;此为数据区
sum DWORD 0 ;定义名为sum的变量(DWORD 伪指令告诉汇编器在程序中为一个双字变量保留空间)
.code ;此为代码区
main PROC
mov eax,5 ;将数字5送入而eax寄存器
add eax,6 ;eax寄存器加6
mov sum,eax
INVOKE ExitProcess,0 ;结束程序
main ENDP
;被 .code 和 .data 伪指令标记的代码和数据区,被称为段
为了使其能运行,要添加必要的声明:
; AddTwo.asm -两个 32 位整数相加
.386 ;这是 .386 伪指令,它表示这是一个 32 位程序,能访问 32 位寄存器和地址
.model flat,stdcall ;选择了程序的内存模式(flat),并确定了子程序的调用规范(称为 stdcall,32 位 Windows 服务要求使用 stdcall 规范)
.stack 4096 ;为运行时堆栈保留了 4096 字节的存储空间,每个程序都必须有
ExitProcess PROTO, dwExitCode:DWORD ;声明了 ExitProcess 函数的原型,可以将其看作为给 Windows 操作系统的返回值
;返回值为零,则表示程序执行成功;而任何其他的整数值都表示了一个错误代码
.code
main PROC
mov eax,5 ;将数字5送入eax寄存器
add eax,6 ;eax寄存器加6
INVOKE ExitProcess,0
main ENDP
END main ;用 end 伪指令来标记汇编的最后一行
引入变量sum后:
;AddTowSum.asm
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD
.data
sum DWORD 0
.code
main PROC
mov eax,5
add eax,6
mov sum,eax
INVOKE ExitProcess,0
main ENDP
END main
但现在大多都是64位系统,那这个代码岂不凉了?
64位基本汇编程序
HelloWorld
;完整段的Hello World程序
STACKS SEGMENT
db 100 dup(?)
STACKS ENDS
DATAS SEGMENT
STRING DB 'Hello World!',13,10,'$'
DATAS ENDS
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:
MOV AX,DATAS
MOV DS,AX
LEA DX,STRING
MOV AH,9
INT 21H
MOV AH,4CH
INT 21H
CODES ENDS
END START
因为初学,给出详细注释:
;完整段的Hello World程序
STACKS SEGMENT ;定义堆栈段
db 100 dup(?) ;堆栈段长100字节, db即字节,如果是用dw就是字
STACKS ENDS ;堆栈段结束
DATAS SEGMENT ;定义数据段
STRING DB 'Hello World!',13,10,'$'
;STRING DB 'Hello World!',0dh,0ah,24h
;13,10分别是回车键,换行键的ASCII码
;0dh,0ah分别是回车键,换行键的ASCII码
;'$'是DOS功能调用INT 21H中9号功能要求的要显示字符串的结束标志(24h也可以,因为24h就是'$')
DATAS ENDS ;数据段结束
CODES SEGMENT ;定义代码段
ASSUME CS:CODES,DS:DATAS,SS:STACKS ;确定cs,ds,ss,指向的逻辑段
START: ;程序入口
MOV AX,DATAS
MOV DS,AX ;将段地址DATAS送入DS中
LEA DX,STRING ;将字符串地址送人DX中
;mov DX,offset STRING
;取得STRING的偏移地址,也就是在DS段中的偏移地址
MOV AH,9 ;AH中的9号功能表示要显示一行字符串
INT 21H ;调用INT 21H的9号中断
MOV AH,4CH
INT 21H ;两个连用表示程序结束
CODES ENDS ;定义代码段结束
END START ;程序结束
解释:
MOV AX,DATAS
MOV DS,AX
分两步的原因:
将伪段地址放入AX中,DATAS不是指令,而是伪指令,实际上是一个动态的内存地址,要想运行,必须先把DATAS放入到DS中,但是80X86中规定,内存数不可以直接装入段寄存器,所以才会有这样的一次中转,mov ds,ax
就是将段地址装入段寄存器,最终达到了段地址装入段寄存器的目的
assume
:
要用assume把逻辑段跟段寄存器(ds,cs,ss等)对应起来,因为原来的DOS找到的空闲内存的地址不是固定的,无法找到一个地址在任何时候都是空闲的。于是DOS需要可以重定位的程序,而当时的定位方式就是设置段寄存器的值,使该程序能在可分配(空闲)的内存中可用。那就需要知道某个段被重定位的时候,需要修改哪个段寄存器的值才能正确执行。
assume提供这种段和重定位代码时需要对应修改的寄存器的关系给编译器,编译器再这个信息写到二进制文件中去,声明格式一般是:
assume ds:data(数据段名称,可任意),cs:code(代码段名称,可任意),ss:stuck(堆栈段名称,可任意)
lea dx,string
:
把string的偏移地址存到dx,这里的 lea 是load effective address的缩写
int 21h
:
dos功能调用,含有近100个功能,提供了应用程序所需要的大多数服务,包括打开文件、关闭文件、读文件、写文件、读键盘输入、写显示屏、读取或设置系统日期和时间,以及一大堆控制变量,通过给AH寄存器赋值,然后调用INT 21H指令,计算机就会根据AH寄存器中的值执行相应的操作,之后查表即可,给出链接:DOS系统功能调用表(INT 21H)
当然,HelloWorld也可以简化一下:
;简化段的Hello World程序
.MODEL SMALL
.DATA
STRING DB 'Hello World!',13,10,'$'
.STACK
.CODE
.STARTUP
LEA DX,STRING
MOV AH,9
INT 21H
.EXIT
END
解释:
汇编程序通常有tiny、small、huge等模式
(1)tiny模式通常和内存映像文件(com)文件对应,代码、数据同段且不超过64k
(2)small模式通常代码段、数据段均不超过64k,代码段中的跳转和call均为near模式,即使用段偏移就能定位
(3)huge模式代码、数据段等不受64k限制,跳转、call指令允许使用far模式
STARTUP和.EXIT是汇编程序MASM中提供的二组简化的代码伪指令,用法可看:程序开始和结束伪操作,在此不赘述
求带符号数绝对值
编写程序段,求AX中存放的带符号数的绝对值,结果存RES单元
data segment
res dw ?
data ends
code segment
;确定cs,ds指向的逻辑段
assume cs:code,ds:data
start:
mov ax,data
mov ds,ax ;将段地址DATAS送入DS中
mov ax,9a00H
cmp ax,0
jge isp ;大于或等于转移指令
neg ax
isp:
mov res,ax
mov ah,4ch ;带返回码结束
int 21h
code ends
end start
求两数的和
;完整段的求3+5的和
STACKS SEGMENT
DB 128 DUP (?)
STACKS ENDS
DATAS SEGMENT
FIVE DB 5
DATAS ENDS
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:
MOV AX,DATAS
MOV DS,AX
MOV AL,FIVE
ADD AL,3
ADD AL,30H
MOV DL,AL
MOV AH,2
INT 21H
MOV AH,4CH
INT 21H
CODES ENDS
END START
加上注释:
;完整段的求3+5的和
STACKS SEGMENT
DB 128 DUP (?)
STACKS ENDS
DATAS SEGMENT
FIVE DB 5
DATAS ENDS
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:
MOV AX,DATAS
MOV DS,AX
MOV AL,FIVE ;将字节变量5存入到寄存器中,也可MOV AL,5
ADD AL,3 ;将寄存器中的值取出,加上3后放回
ADD AL,30H ;需要转化成ASCII码才能进行显示,8对应ASCII码为38H,故加上30H
MOV DL,AL ;将待输出字符的ASCII码传到DL中去
MOV AH,2 ;将02命令放入AH中,表示输出DL
INT 21H ;DOS系统调用放入AH的命令
MOV AH,4CH ;结束本程序,返回 DOS 操作系统
INT 21H ;DOS系统调用放入AH的命令
CODES ENDS
END START
也可以简化成这样:
;简化段的求3+5的和
.MODEL SMALL
.DATA
FIVE DB 5
.STACK
DB 128 DUP (?)
.CODE
.STARTUP
MOV AL,FIVE
ADD AL,3
ADD AL,30H
MOV DL,AL
MOV AH,2
INT 21H
.EXIT 0
END
计算1+2+3+…+10,将结果显示在屏幕上
code segment
;段间远调用: 调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用
main proc far
assume cs:code;确定cs指向的逻辑段
start:
;push寄存器:将一个寄存器中的数据入栈
;pop寄存器:出栈用一个寄存器接收数据
push ds
sub ax,ax ;初值0
push ax
mov bx,0ah ;初值10
mov cx, 0ah ;初值10
sum1:
add ax,bx ;从10加到1
dec bx
;loop指令用来实现循环功能,cx(寄存器)存放循环次数,CPU执行loop指令的时候
;先cx=cx-1,然后判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行
loop sum1
printit:
;需要将ax中的结果以十进制形式输出
;将结果的两位数分别存于低位和高位中
mov bl,10
div bl
;AL是商
mov ch,ah ;将余数AH保存到al中
add al,30h
mov dl,al
mov ah,2
int 21h
;AH是余数
add ch,30h ;需要转化成ASCII码才能进行显示,故加上30H
mov dl,ch
mov ah,2 ;显示输出,且dl=输出字符
int 21h
;子程序定义结束
ret
main endp
code ends
end
main proc far
:
proc是定义子程序的伪指令,位置在子程序的开始处,它和endp分别表示子程序定义的开始和结束两者必须成对出现。
far是该子程序的属性,决定调用程序和子程序是否在同一代码段
如下:为子程序定义及说明,
子程序名 PROC NEAR ( 或 FAR )
……
ret
子程序名 ENDP
子程序名为符合语法的标识符
NEAR属性(段内近调用): 调用程序和子程序在同一代码段中,只能被相同代码段的其他程序调用;
FAR属性(段间远调用): 调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用.
div
:
1:除数为8位或者16位(即字节型或字型),在寄存器或内存单元中
2: 被除数在AX 或者 AX和DX中( 注意,后面是AX和DX,AX存放低16位,DX存放高16位)
除数 被除数
8位 16位
16位 32位(AX和DX)
与此对应的,当除数为8位时,商存放在AL寄存器中,余数存放在AH寄存器中,当除数为16位时,商在AX中,余数在DX中
3:指令格式
div reg //reg表示一个寄存器
div 内存单元
如:
mov ax,17
mov bl,3
div bl
求分段函数值
已知变量x为16位带符号数,分段函数的值要求保存到字单元y中,函数定义如下:当x=0时,y=0,当x大于0时,y=1,当x小于0的时候,y=-1
data segment ;数据段定义
x dw -128
y dw ?
data ends
code segment ;代码段定义
assume cs:code,ds:data
start:
mov ax,data
mov ds,ax ;将段地址datas送入ds中
mov ax,x
cmp ax,0
jg ispn ;jg:大于转移指令
jz iszn ;jz(jUMP IF zERO)是此前的运算结果为0时跳转,反之不跳转
mov y,-1
jmp finish
ispn:
mov y,1
jmp finish
iszn:
mov y,0
finish:
mov ah,4ch
int 21h
code ends
end start
绘制矩形
CODES SEGMENT
ASSUME CS:CODES
START:
mov ah,00
mov al,06h
int 10h ;设置640*480、16色彩色分辨率
mov dx,50
back_1:
mov cx,100
back_2:
mov ah,0ch
mov al,71h ;白底蓝色图
mov bh,0
int 10h
inc cx
cmp cx,200
jnz back_2
inc dx
cmp dx,150
jnz back_1
CODES ENDS
END START
给出注释:
CODES SEGMENT
ASSUME CS:CODES
START:
mov ah,00 ;是用来设定显示模式的服务程序
mov al,06h ;设置图形模式
int 10h ;设置640*480、16色彩色分辨率
mov dx,50 ;y坐标
back_1:
mov cx,100 ;x坐标
back_2:
mov ah,0ch ;设定显示模式
mov al,71h ;设置图形模式,白底蓝色图
mov bh,0 ;设定终止x坐标
int 10h
inc cx ;x++
cmp cx,200
jnz back_2 ;如果cx没到200,就接着back_2,x++,画横线(x从100->200)
inc dx ;如果cx等于200,y++,往下走一行接着画横线
cmp dx,150
jnz back_1 ;如果dx没到150,就到back_1让x=100,接着back_2了
;如果y轴到了150,矩形画完了,程序结束
CODES ENDS
END START
解释:
首先得理解一下汇编语言中的标志位,可看这个:汇编语言中的标志位,然后了解一下cmp比较指令对标志寄存器的影响,再看一下这个:cmp比较指令对标志寄存器的影响,而且对条件转移指令要有一定了解,还可以看这个:条件转移指令详解,之后再读跳转部分的代码就会好很多了😅,其实难点就在cmp和jnz的结合上
debug基本操作
debug下最常用的调试指令为六个
(1)R :查看更改cpu寄存器内容
(2)D:查看内存中内容
(3)E:改写内存中内容
(4)U:将内存中机器指令翻译成汇编指令
(5)T:执行一条机器指令
(6)A:以汇编格式在内存中写入一条指令
具体可看:常用命令的使用
对照 求两数的和 部分的代码,来演示一下
大概就这样,部分寄存器的变化原理没有搞懂,以后深入研究吧