汇编语言学习5

第一个汇编程序

Posted by BINBIN Blog on August 9, 2019

##汇编程序从写出到执行的过程

编程-> 1.asm -> 编译 masm -> 1.obj -> 连接(link)-> 1.exe-> 加载 (command) -> 内存中的程序 -> 运行(run) CPU

加载后,CPU的CS:IP指向程序的第一条指令(即程序的入口)

限于篇幅,其实只要拿一个实例就懂了

;1.asm
assume cs:codesg ;将用作代码段的段codesg和段寄存器cs联系起来。

codesg segment ;定义一个段,段的名称为“codesg”,这个段从此开始
			   ;codesg是一个标号,作为一个段的名称,最终被编译连接成一个段的段地址

	mov ax, 0123H
	mov bx, 0456H 
	add ax, bx
	add ax, ax 
	
	mov ax, 4c00H 
	int 21H ;这两条指令实现程序的返回
	
codesg ends ;名称为“codesg”的段到此结束

end ;编译器在编译汇编程序的过程中,碰到了伪指令end,结束对源程序的编译

这段代码里面有伪指令,伪指令需要编译器来执行的。

(1)

 xxx segment
    .....
    .....

    xxx ends

这个就不细说了,一个段开始,结束,必须要段名称,该程序的段名称是codesg,书中也说codesg也是标号

(2)

end

这个也是伪指令

(3)

assume

这个也是伪指令, 假设的意思,详细入注释所说,关联相关的段寄存器而已 编译后的示意图(汇编-机器码)

##程序执行过程跟踪

DOS系统中.EXE文件中的程序的加载过程

[bx]和loop指令

[bx]和loop指令

[bx] 的含义:[bx]同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中

loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两步操作,

(cx) =(cx) - 1;

判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。 计算 $\2^12$ 2的12次幂。

assume cs:code 

code segment 
	mov ax, 2
	
	mov cx, 11 ;循环次数
s:  add ax, ax 
	loop s     ;在汇编语言中,标号代表一个地址,标号s实际上标识了一个地址,这个地址处有一条指令:add ax,ax。
               ;执行loops时,首先要将(cx)减1,然后若(cx)不为0,则向前
               ;转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
	
	mov ax,4c00h 
	int 21h 
code ends 
end

##loop和[bx]的联合应用

计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中

问题分析:

  • 这些内存单元都是字节型数据范围0 ~ 255 ,12个字节数据和不会超过65535,dx可以存下
  • 对于8位数据不能直接加到 dx 解决方案:

用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx

assume cs:code 

code segment 
	mov ax, 0ffffh ;在汇编源程序中,数据不能以字母开头,所以要在前面加0。
	mov ds, ax 
	mov bx, 0   ;初始化ds:bx指向ffff:0
	mov dx, 0   ;初始化累加寄存器dx,(dx)= 0
	
	mov cx, 12  ;初始化循环计数寄存器cx,(cx)= 12
s:  mov al, [bx]
	mov ah, 0
	add dx, ax  ;间接向dx中加上((ds)* 16 +(bx))单元的数值
	inc bx      ;ds:bx指向下一个单元
	loop s 
	
	mov ax, 4c00h 
	int 21h 
code ends 
end

段前缀

mov ax, ds:[bx]
mov ax, cs:[bx]
mov ax, ss:[bx]
mov ax, es:[bx]
mov ax, ss:[0]
mov ax, cs:[0]

这些出现在访问内存单的指令中,用于显式地指明内存单元的段地址 的“ds:”,“cs:”,“ss:”,“es:”,在汇编语言中称为段前缀。

段前缀的使用

将内存ffff:0 ~ ffff:b单元中的数据复制到0:200 ~ 0:20b单元中。

ssume cs:code 

code segment 
	mov ax, 0ffffh 
	mov ds, ax   ;(ds)= 0ffffh 
	mov ax, 0020h
    mov es, ax   ;(es)= 0020h 
    mov bx, 0    ;(bx)= 0,此时ds:bx指向ffff:0,es:bx指向0020:0
    
	mov cx,12   ;(cx)=12,循环12次
s:  mov dl,[bx] ;(d1)=((ds)* 16+(bx)),将ffff:bx中的字节数据送入dl 
	mov es:[bx],dl ;((es)*16+(bx))=(d1),将dl中的数据送入0020:bx 
	inc bx  ;(bx)=(bx)+1
	loop s 
	
	mov ax,4c00h 
	int 21h 
code ends 
end

##包含多个段的程序

程序中对段名的引用,将被编译器处理为一个表示段地址的数值。

1.在一个段中存放数据、代码、栈的情况。 2.将数据、代码、栈放入不同的段中。

在代码段中使用数据 计算8个数据的和存到ax寄存器,且数据是给定的,自然要考虑给定数据存放在哪里的问题

代码如下:

;计算8个数据的和存到ax寄存器
assume cs:code 

code segment 

	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据

	start:	mov bx, 0  ;标号start
			mov ax, 0  
			
			mov cx, 8
	s:		add ax, cs:[bx]
			add bx, 2
			loop s 
			
			mov ax, 4c00h 
			int 21h 
code ends
end start    ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
	     	 ;用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。


dw 是定义字型数据 define word,8个数据,占用的内存空间大小是16个字节。

注:end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方,注释有说明。

代码里面有个start 标号,就如注释所说,指定程序入口,否的话,默认程序入口就是dw 定义8个字节说的地址,可这些存放的都是数据,并不是指令码,一旦执行,会导致程序崩溃!

在代码段中使用栈

assume cs:codesg 

codesg segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
				;用dw定义16个字型数据,在程序加载后,将取得16个字的
				;内存空间,存放这16个数据。在后面的程序中将这段空间当作栈来使用
				;16-47单元作为栈使用
	start:	mov ax, cs 
			mov ss, ax 
			mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30。   30h = 48d
			mov bx, 0
			
			mov cx, 8
	s:		push cs:[bx]
			add bx, 2
			loop s    ;以上将代码段0~15单元中的8个字型数据依次入栈
			
			mov bx, 0
			
			mov cx, 8
	s0:		pop cs:[bx]		
			add bx,2
			loop s0   ;以上依次出栈8个字型数据到代码段0~15单元中
			
			mov ax,4c00h 
			int 21h 
codesg ends 
end start	;指明程序的入口在start处

我们需要考虑要将用多个段来存放数据、代码和栈

assume cs:code,ds:data,ss:stack 

data segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
data ends 

stack segment 
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
stack ends 

code segment 
	start:	mov ax, stack;将名称为“stack”的段的段地址送入ax
			mov ss, ax
			mov sp, 20h  ;设置栈顶ss:sp指向stack:20。 20h = 32d
			
			mov ax, data ;将名称为“data”的段的段地址送入ax
			mov ds, ax   ;ds指向data段
			
			mov bx, 0    ;ds:bx指向data段中的第一个单元
			
			mov cx, 8
	s:	    push [bx]
			add bx, 2
			loop s       ;以上将data段中的0~15单元中的8个字型数据依次入栈
			
			mov bx, 0
			
			mov cx, 8
	s0:		pop [bx]
			add bx, 2
			loop s0      ;以上依次出栈8个字型数据到data段的0~15单元中
			
			mov ax, 4c00h 
			int 21h 
code ends
end start
;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
;可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令


再次提醒,CPU 不允许把数据直接存放到ds段寄存器的,通过中间寄存器中转

##更灵活的定位内存地址的方法

and和or

and指令:逻辑与指令,按位进行与运算。

mov al, 01100011B and al, 00111011B

执行后:al=00100011B即都为1才为1

or指令:逻辑或指令,按位进行或运算。

mov al, 01100011B or al, 00111011B 执行后:al=01111011B 即只要有一个为1就为1

关于ASCII码,我就不多说了,可以直接查看ASCII表就知道了。

以字符形式给出的数据

assume cs:code,ds:data 

data segment 
	db 'unIx'   ;相当于“db 75H,6EH,49H,58H”
	db 'foRK'
data ends 

code segment
start:	mov al, 'a'  ;相当于“mov al, 61H”,“a”的ASCI码为61H;
		mov b1, 'b'
		
		mov ax, 4c00h 
		int 21h 
code ends
end start

这里可以谈一下大小写转换的问题。 小写字母的ASCII码值比大写字母的ASCII码值大20H。

大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)。

代码目的将第一个字符串转化为大写,第二个字符串转化为小写

assume cs:codesg,ds:datasg 

datasg segment 
	db 'BaSiC'
	db 'iNfOrMaTion'
datasg end

codesg segment 
	start:	mov ax, datasg 
			mov ds, ax	;设置ds 指向 datasg段
		
			mov bx, 0	;设置(bx)=0,ds:bx指向’BaSic’的第一个字母
			
			mov cx, 5	;设置循环次数5,因为’Basic'有5个字母
	s:		mov al, [bx];将ASCII码从ds:bx所指向的单元中取出
			and al, 11011111B;将al中的ASCII码的第5位置为0,变为大写字母
			mov [bx], al	;将转变后的ASCII码写回原单元
			inc bx		;(bx)加1,ds:bx指向下一个字母
			1oop s 
			
			mov bx, 5	;设置(bx)=5,ds:bx指向,iNfOrMaTion'的第一个字母
			
			mov cx, 11	;设置循环次数11,因为‘iNfOrMaTion'有11个字母
	s0:		mov al, [bx]
			or al, 00100000B;将a1中的ASCII码的第5位置为1,变为小写字母
			mov [bx], al 
			inc bx
			loop s0
			
			mov ax, 4c00h 
			int 21h 
codesg ends

不多说了,直接看注释即可!

###[bx+idata] [bx+idata]表示一个内存单元, 例如:mov ax, [bx+200] 该指令也可以写成如下格式:

mov ax, [200+bx]

mov ax, 200[bx]

mov ax, [bx].200

用[bx+idata]的方式进行数组的处理

assume cs:codesg,ds:datasg 

datasg segment 
	db 'BaSiC';转为大写
	db 'MinIx';转为小写
datasg ends

codesg segment
	start:
		mov ax, datasg 
		mov ds, ax 
		mov bx, 0  ;初始ds:bx
	
		mov cx, 5
	s:	mov al, 0[bx]  
		and al, 11011111b ;转为大写字母
		mov 0[bx], al ;写回
		mov al, 5[bx]  ;[5 + bx]
		or al, 00100000b ;转为小写字母
		mov 5[bx], al 
		inc bx
		loop s
		
		mov ax, 4c00h 
		int 21h
codesg ends
end start

等效C 语言如下:

int main()
{
	char a[] = "BaSic";
	char b[] = "MinIX";
	
	int i = 0;
	
	do
	{
		a[i] = a[i] & 0xDF;
		b[i] = b[i] | 0x20;
		i++;
	} while(i < 5);

	return 0;
 }

###SI和DI si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。

以下三组指令是相同的!

  1. mov bx,0 ` mov ax,[bx] `

  2. mov si,0 ` mov ax,[si] `

  3. mov di,0 ` mov ax,[di] `

用si 和 di 实现将字符串’welcome to masm!’ 复制到他后面的数据区中。

assume cs: codesg, ds: datasg 

datasg segment 
	db 'welcome to masm!';用si和di实现将字符串‘welcome to masm!"复制到它后面的数据区中。
	db '................'
datasg ends

codesg segment 
	start:	mov ax, datasg 
			mov ds, ax 
			mov si, 0
			
			mov cx, 8
	s:		mov ax, 0[si] ;[0 + si]
			mov 16[si], ax ;[16 + si]
			add si, 2 
			loop s 
			
			mov ax, 4c00h 
			int 21h 
codesg ends 
end start

###[bx + si]和[bx + di]

[bx+si]和[bx+di]的含义相似

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)

指令mov ax, [bx + si]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中

该指令也可以写成如下格式:mov ax, [bx][si]

###[bx+si+idata]和[bx+di+idata] [bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata

指令mov ax,[bx+si+idata]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中。

不同的寻址方式的灵活应用

[idata]用一个常量来表示地址,可用于直接定位一个内存单元; [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元; [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元; [bx+si]用两个变量表示地址; [bx+si+idata]用两个变量和一个常量表示地址。

代码示例如下:

;将datasg段中每个单词改为大写字母
assume cs:codesg,ds:datasg,ss:stacksg 

datasg segment
	db 'ibm            ' ;16个数据
	db 'dec            ' 
	db 'dos            '
	db 'vax            '  ;看成二维数组
datasg ends 

stacksg segment ;定义一个段,用来做栈段,容量为16个字节
	dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends 

codesg segment 
	start:	mov ax, stacksg 
			mov ss, ax
			mov sp, 16 ;ss:sp指向栈底
			mov ax, datasg 
			mov ds, ax 
			mov bx, 0 ;初始ds:bx
			
			;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
			mov cx, 4
	s0:		push cx	;将外层循环的cx值入栈
			mov si, 0
			mov cx, 3	;cx设置为内层循环的次数
	s:		mov al, [bx+si]
			and al, 11011111b ;每个字符转为大写字母
			mov [bx+si], al 
			inc si
			loop s 
			
			add bx, 16 ;下一行
			pop cx	;恢复cx值
			loop s0 ;外层循环的loop指令将cx中的计数值减1
			
			mov ax,4c00H 
			int 21H 
codesg ends
end start

##数据处理的两个基本问题 reg表示寄存器 sreg表示段寄存器

reg的集合是 ax bx cx dx ah al bh bl ch cl dh dl sp bp si di;

sreg的集合包括 ds ss cs es;

bx、si、di和bp

在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。

在[….. ]中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si、bx和di、bp和si、bp和di。

只要在[….. ]中使用寄存器bp,而指令中没有显性地给出段地址, 段地址就默认在ss中!

###机器指令处理的数据在什么地方 数据处理大致可分为3类:读取、写入、运算。

在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口!

###汇编语言中数据位置的表达

汇编语言中用3个概念来表达数据的位置

  • 立即数(idata)
mov ax, 1                 ;对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中)
add bx, 2000h             ;在汇编语言中称为:立即数(idata)
or bx, 00010000b
mov al, 'a'

  • 寄存器
mov ax, bx     ;指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
mov ds, ax 
push bx 
mov ds:[0], bx 
push ds 
mov ss, ax
mov sp, ax
  • 段地址(SA)和偏移地址(EA)
;指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
mov ax, [0]
mov ax, [di]
mov ax, [bx+8]
mov ax, [bx+si]
mov ax, [bx+si+8]   ;以上段地址默认在ds中

mov ax, [bp]
mov ax, [bp+8]
mov ax, [bp+si]
mov ax, [bp+si+8]   ;以上段地址默认在ss中

mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx+si]
mov ax, cs:[bx+si+8] ;显式给出存放段地址的寄存器

寻址方式,总结一下!

###指令要处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据,byte和word!

1.通过寄存器名指明要处理的数据的尺寸。 例如: mov al, ds:[0] 寄存器al指明了数据为1字节

2.在没有寄存器名存在的情况下,用操作符Xptr指明内存单元的长度,X在汇编指令中可以为word或byte。 例如:mov byte ptr ds:[0], 1 byte ptr 指明了指令访问的内存单元是一个字节单元。

3.有些指令默认了访问的是字单元还是字节单元 例如,push [1000H],push 指令只进行字操作。

###寻址方式的综合应用

任务就是 编程修改过时的数据

mov ax, seg 
mov ds, ax 
mov bx, 60h   ;确定记录地址,ds:bx 

mov word ptr [bx+0ch], 38   ;排名字段改为38  [bx].0ch
add word ptr [bx+0eh], 70   ;收入字段增加70  [bx].0eh
mov si, 0   ;用si来定位产品字符串中的字符
mov byte ptr [bx+10h+si], 'V'   ;[bx].10h[si]
inc si 
mov byte ptr [bx+10h+si], 'A'
inc si 
mov byte ptr [bx+10h+si], 'X'

c语言写法:

/*定义一个公司记录的结构体*/
struct company
{
    char cn[3];/*公司名称*/
    char hn[9];/*总裁姓名*/
    int pm;/*排名*/
    int sr;/*收入*/
    char cp[3];/*著名产品*/
};
//sizeof (struct company) == 24

int main()
{
    /*定义一个公司记录的变量,内存中将存有一条公司的记录*/
    struct company dec = {"DEC", "Ken 0lsen", 137, 40, "PDP"};

    int i;

    dec.pm = 38;
    dec.sr = dec.sr + 70;

    i = 0;
    dec.cp[i] = 'V'; //mov byte ptr [bx].10h[si], 'V'
    i++;
    dec.cp[i] = 'A';
    i++;
    dec.cp[i] = 'X';

    return 0;
}

div指令、dd、dup、mul指令

###div是除法指令

1.除数:有8位和16位两种,在一个寄存器或内存单元中。

2.被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放; 如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。

3.结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数; 如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

可能有的对数学不清楚。被除数÷除数=商( 余数); 图片所示:

编程,利用除法计算指令计算100001/100。

分析一下,100001 大于65536, 所以一个ax寄存器不够的,这个只能存放2个字节! 需要dx 和 ax 联合存放! 除数100 小于255,可以放在8位寄存器存放,但是被除数是32位,所以除数一定要用16位的哦!

换算10001 十六进制是186A1H。

如下编码:

;利用除法指令计算100001/100。
mov dx, 1
mov ax, 86A1H ;(dx)*10000H+(ax)=100001
mov bx, 100
div bx

正如书中所说,ax存商,即 (ax) = 03E8H (1000),(dx)=1(余数为1)

作者书中约定过了(ax) 带括号代表ax 里面的值。

;利用除法指令计算1001/100
mov ax, 1001
mov bl, 100
div b1

伪指令dd

db和dw定义字节型数据和字型数据。

dd是用来定义dword(double word,双字)型数据的伪指令

操作符dup

dup在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号! 它是和db、dw、dd等数据定义伪指令配合使用的,用来进行数据的重复!

db 3 dup(0) ;定义了3个字节,它们的值都是0,相当于db 0,0,0。
db 3 dup(0, 1, 2) ;定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2。
db 3 dup('abc', 'ABC') ;定义了18个字节,它们是abcABCabcABCabcABCC,相当于db 'abc', 'ABC' ,'abc' , 'ABC, 'abc', 'ABC'。

dup的格式就是如下。 db 重复次数 dup (重复的字节型数据) dw 重复次数 dup (重复的字型数据) dd 重复次数 dup (重复的双字型数据)

###mul 指令 mul是乘法指令,使用 mul 做乘法的时候:相乘的两个数:要么都是8位,要么都是16位。

  • 8 位: AL中和 8位寄存器或内存字节单元中;

  • 16 位: AX中和 16 位寄存器或内存字单元中。

结果

  • 8位:AX中;

  • 16位:DX(高位)和AX(低位)中。

格式:mul 寄存器 或 mul 内存单元

;计算100*10
;100和10小于255,可以做8位乘法
mov al,100
mov bl,10
mul bl

;结果: (ax)=1000(03E8H) 

;计算100*10000
;100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx

;结果: (ax)=4240H,(dx)=000FH     (F4240H=1000000)

##另附上win10 运行dos debug 和 masm的方法##

就不需要用虚拟机弄,debug里面已经自带了masm

First

首先我们需要给电脑配置debug文件,以win10为例,默认条件下系统没有debug相关的文件,我们需要自己去配置 下载文件我已经上传了,链接在这里 下载好并解压,结果如下: 此时我们需要记下当前的路径,我们这里当前路径为D:\Debug

###Second

我们需要下载一个DosBox,根据维基百科上定义讲: DOSBox是一种模拟器软件,主要是在IBM PC兼容机下,模拟旧时的操作系统:MS-DOS,支持许多IBM PC兼容的显卡和声卡,为本地的DOS程序提供执行环境,使这些程序可以正常运行于大多数现代计算机上的不同操作系统。DOSBox特别是为运行早期的计算机游戏所设计,主要以C++编写,是以GNU通用公共许可证许可发布的自由软件。 DOSBox可以运行那些在现代计算机上不能运行的MS-DOS软件,这些软件通常与现在的主流硬件和操作系统有一些不兼容。DOSBox在模拟MS-DOS同时,还增加了一些可用特性,包括虚拟磁盘、点对点网络、对模拟画面截图和录像。有些非官方的DOSBox变体,如DOSBox SVN Daum和DOSBox SVN-lfn提供了更多的功能,比如存档、长文件名支持等[4]。有些游戏开发商重新发行早期的DOS游戏时,也会使用DOSBox,使其可以在现代计算机上运行。 文件我已经上传到本地了,下载链接在这里 下载后的界面如下: Third 我们需要使用mount命令(用来挂载硬盘或镜像等),将DEBUG.EXE所在的路径挂载到C盘的盘符,以便调用的时候方便直接在同一目录下 然后我们只需要输入C:\进入当前的盘符,使用debug命令即可直接启动DEBUG.EXE