汇编三个作业详解

1,031 阅读8分钟

前言

这篇文章记录下汇编课程留下的三个作业,如下所示。

  1. 循环打印1-9
  2. 从键盘输入一个字符并根据其大写还是小写进行相反转换。
  3. 进制转换

在此之前,我们先说一下masm和dosbox。

时代在进步,而课程还停留在以前8086的时代,不知在过多少年才能是个头,还是说经典的东西,永不过时。

你是不是有这样的疑问,为什么需要在dosbox上运行你写的masm代码?为什么不能直接编译、链接、运行?非得搞出个dosbox?

其实这是可以的,只是没有人和你说,最经典的一本汇编书籍,《汇编语言》王爽著的,最后的实例都需要在dos系统上运行,而你要想运行,就需要自己安装真实dos系统,或者dosbox模拟器,论这原因,还是8086的原因,8086是实模式下的CPU,这个实模式,是相对保护模式而言,在8086问世时,没有实模式概念,在80286问世后,由于他的工作模式和之前不一样,所以为了区别老款CPU的工作模式,把他称为保护模式,而把8086这种工作模式称为实模式,我们现在的系统,都是在保护模式下运行,但是在电脑启动时,CPU还是会在实模式下运行,这也为了兼容,要切换到保护模式,需要自己控制。

还是遵循一句话,讲不清的就不要讲,这里二者区别,不是三言两语就能说请的,但是现在我们只需要知道为什么需要dosbox,因为我们学习的是8086下的汇编,而要正确运行书中代码,需要一个工作在实模式下的环境,那么就两个选择,要么自己安装dos系统,要么通过软件模拟,这个软件就是dosbox。

中断

还有一个原因,在我们课本中最后需要很多实例,比如要调用bios中断或者dos中断来完成一些功能。

什么是调用bios中断或者dos中断呢?

如果你是js开发者,可以理解为当你需要打开一个新窗口时要调用window.open。

如果你是java开发者,可以理解为当你需要删除文件时,需要Files.delete方法。

如果你是c开发者,可以理解为当你需要输出字符时,需要print方法。

而bios中断、dos中断是他们事先提供好了一部分功能,当你需要调用时,通过int xx指令调用,被称为中断调用,或者功能调用,而你要调用bios中断,在windows上或者linux上是没办法完成的,因为保护模式是没办法使用bios中断的,bios中断只能在实模式下工作,而dos中断的话,不在dos系统中也没办法。

而他们提供了众多功能,每个功能都有唯一的编号,称为中断号,比如int 10h,这里的10h号中断是bios提供的,被称为Video Services,而10h号中断下又细分了很多功能,比如当你想输出字符时,需要先设置ah寄存器为0eh,然后把要输出的字符的ascii通过al寄存器告诉bios,但这个功能号只允许输出单个字符,而你想一次输出一句话,可以使用ah为13h的功能,这需要一个参考手册,我找到了三篇不错的文章,如下,这里面详细的介绍了bios中断和dos中断调用。

http://www.ablmcc.edu.hk/~scy/CIT/8086_bios_and_dos_interrupts.htm#attrib

https://grandidierite.github.io/bios-interrupts/

https://www.csee.umbc.edu/courses/undergraduate/CMSC211/fall01/burt/tech_help/BIOSandDOS_Interrupts.html

而dos中断和bios中断在强大的dosbox中都能模拟,这也就限制了我们的一些代码只能在dosbox中运行,当然可以不用,用它的主要原因就是了解以前CPU物理地址的计算方式和dos、bios中断等。

使用masm开发应用,是比较简单的,masm提供了大量伪指令,比如.IF这种高级语言才有的判断语句,这是其他汇编如nasm中是没有的,但是,本篇的论点是通过bios中断或者dos中断来完成这三个作业,所以就不讨论windows下汇编开发应用了,详细可参考书籍《汇编语言基于x86处理器》,也有很多windows工具也都是masm开发的,而要完成一些功能,比如打印,就不能使用dos中断或者bios中断了,需要使用windows提供的API,而在linux下,会使用到80h中断。

目前在技术中讨论中断更多的也是讨论linux下的80h中断。

我们都想开发出在实体机上跑的程序,而不是整天在模拟器中玩来玩去,所以,在我们完成这三个作业时,可以不使用masm + dosbox,而是使用nasm + qemu代替,nasm也是一个汇编器,同masm,qemu是一个模拟器,它可以模拟整个机器(CPU、内存等)以此来运行代码,通常我们在开发操作系统的时候会使用,这两种组合调试出来的程序,可以直接在没有操作的情况下运行,但这还需要了解操作系统启动过程。

如果作为学习的话,使用nasm + qemu更简单,同时也对操作系统的启动有更深的理解,但这种方式没办法调用dos中断,因为我们没有dos环境,但可以调用bios中断,其实这就够了,你总不能用学通dos中断后写程序吧,如果你有两个生命可以浪费,才推荐,而我觉得更重要的学bios中断。

因为每个程序员都有一个梦想,写一个操作系统,不管这个操作系统可以干什么,都会让你加深计算机的印象,而这个过程是离不开bios中断的,第一步就是和bios中断打交道。

那为什么不使用masm + qemu这种方式呢?因为masm局限性,只支持window上运行,没办法直接输出纯二进制内容,masm编译出来的文件,只有window才能识别,而nasm可以编译出纯二进制内容,可以不经过操作系统,直接由机器执行。

nasm也更简单,如果你使用过masm,开头会写一些segmentend start指令控制开始的指令,而nasm可以直接在第一行就写核心代码。

这两个安装都很简单,同安装微信一样,没有其他多余的选择,一路next就可以了,就不演示了。

下面看第一个作业。

循环打印1-9

这比较好做,先看一下整体代码。

org 0x7c00

start:
	mov ah,15
	int 10h
	mov ah,0
	int 10h
	mov cx,9
	mov bx,1
_next
	mov dx,bx
	add dx,48
	mov ax,dx
	mov ah,0eh
	int 10h
	inc bx
	push cx
	mov ah,86h
	mov cx,3h
	mov dx,0h
	int 15h
	pop cx
	loop _next

	jmp start


times 510 - ($ -$$) db 0
dw 0xaa55

30行左右,第一行和最后两行固定,不需要现在理解,这是关于操作系统启动部分知识的,中间是核心代码,运行的话,使用下面两条命令。

nasm print1-to-9.asm  -o demo.img
qemu-system-i386 demo.img

然后你就会看到循环一直打印1-9。

我们看下面这几行代码。

start:
	mov ah,15
	int 10h
	mov ah,0
	int 10h
	mov cx,9
	mov bx,1

看到int xxx时,直接看手册,查找ah为15时,发生中断后做了什么事,查找到对应说明后,你会发现这是获取Video模式,然后ah为0时,发生中断后是设置Video模式,是不是还不懂? 那就对了,这需要先了解Video模式,但这里这么做的原因仅仅是为了清除屏幕,因为获取当前Video模式,在设置Video模式可以达到清除屏幕的疗效。

(汇编中调用中断后返回的信息同样在寄存器中)

下面做的就是打印1-9,数字ascii可以分别以48作为基础数值,ascii为48表示0,那么1的ascii是48+1=49。

再看下面,我们先设置cx为9,cx可以和loop指令一起使用,作用是控制循环次数,当loop指令执行时,如果cx大于0,那么就跳转到指定标号,设置bx为1的作用是,记录当前要打印的数字,1表示从1开始打印。

	mov cx,9  ;控制循环次数,在遇到loop指令时候自动递减
	mov bx,1  ;从1开始打印
        
	mov dx,bx  ;把bx放入dx,dx会加基础值48,形成最终ascii码,bx保持不变,
	add dx,48  ;形成数字的ascii码
	mov ax,dx  ;al中存放要打印的ascii码,但不能直接使用al,因为dx是16寄存器,al是8位,16位寄存器不能向8位寄存器中放入数据,但这样移动后,ax高8位是0,低8位也就是al也还是dx中的值,这个ascii码超不过8bit,最多255
        
	mov ah,0eh  ;设置功能号
	int 10h     ;中断

这样就完成了一个字符1的打印,下面做的是循环,但是CPU运行是很快的,非常有必要做一个等待函数,最基础做法是通过一个很大次数的循环,让cpu空转一段时间,形成一个等待,但是bios提供了一个等待中断,我们不必自己去写,当ah为86h时,再调用int 15h中断时,bios会根据cx+dx中的值(这个值就是要等待的时间)进行等待,为什么是86h呢? 可能作者也喜欢西游记吧。

        inc bx        ;bx自增1,表示要进行下一个数字打印
	push cx       ;cx入栈,因为要使用cx寄存器,cx我们在上面用来循环,不能被破坏。
        
	mov ah,86h
	mov cx,3h
	mov dx,0h
	int 15h       ;进行等待300毫秒
        
	pop cx        ;恢复cx寄存器
	loop _next    ;继续循环,如果cx不是0,跳转到_next下,并自减1.

	jmp start     ;强制重新开始,清除屏幕,重新打印1-9

第一题就做完了。

大小写转换。

这个也比较简单,先看一下整体代码。

org 0x7c00

start:
	mov ah,0x00
	int 16h
print:
	mov  ah , 0eh
	int 10h
        jmp  start

times 510 - ($ -$$) db 0
dw 0xaa55

但这段代码并不能完成输入大写,自动输出小写,输入小写,自动输出大写的功能,仅仅是做到输入什么然后原样输出什么,这里面需要用到16h号中断,这是bios提供的,只需两步

	mov ah,0x00  ;表示获取键盘输入的字符
	int 16h      ;16h中断是键盘功能的中断,没有输入时会等待在这里。

上面代码调用完成后,如果发生键盘按下,al寄存器中就是这个字符的ascii码,这时候可以直接输出,另外还有个02h的功能,可以检测cap lock是否处于开启的状态,但不止于此,他的作用是获取键盘标志,这些标志可以是cap lock键,Crtl键,等,如果被按下,那么对应的标志位为1。

而如果想输入小写字母a,然后输出a=A,输入大写字母A,输出A=a,这就需要判断al中的范围了,我们知道小写字母的ascii范围在97-122之间,而大写字母在65-90之间,每个字符的大小写中间相差32。

所以就可以这样做,判断al的范围,如果大于等于97,那么我们简单认定输入了小写字母,将al减去32输出,这里为什么要简单认为,因为大于97还有非字母的字符,这里就不验证了。

如果没小于97,我们就认定输入了大写字母,将al加32输出。

org 0x7c00

start:

	mov ah,0x00
	int 16h  ;等待键盘输入

case:
	mov dl,al  ;将输入的原来数据存放到dl中
	cmp al,97  ;将al和97比较
	jae tolow  ;jae表示大于或等于,成立的话跳转到tolow
	jmp tocap  ;上面不成立,跳转tocap

tolow:
	sub al,32  ;将al-32,转换为大写
	jmp print  ;打印
       
tocap:
	add al,32  ;将al+32, 转换为小写
	jmp print  ;打印

print:
	push ax    ;要打印的字符入栈
	mov al,dl  ;先打印原本输入的字符
	mov  ah , 0eh
	int 10h
	
	mov al,'='  ;打印一个等号
	mov  ah , 0eh
	int 10h
	
	pop ax       ;要打印的字符出栈,并打印
        
	mov  ah , 0eh
	int 10h
	
	
	mov al,' '   ;打印一个空格
	mov  ah , 0eh
	int 10h
        jmp  start   ;再来一遍
    

times 510 - ($ -$$) db 0
dw 0xaa55

下面就是效果图。

image.png

进制转换

到这里就需要考验水平了,bios可没有提供这样的功能,需要我们自己按照对应规则去写,但由于转换之间太多,比如2和10进制之间、10和16进制之间、2和16进制之间等,所以,这里只演示一个10转2的效果。

10进制转2进制方法就不说了,比较简单,就是一直除,保存余数,在把所有余数倒序就是最终结果。

其实也很简单,下面看整体代码。

org 0x7c00

start:
    	mov ax,78 ;将要转换二进制数的十进制放入ax中
    	mov cx,0  ;记录一共除了几次
_next:    
	inc cx    ;cx自增
   	mov dx,0  ;做除发时候,被除数在dx:ax中,dx是高位,ax是低位,这里不需要高位,但需要为0
   	mov bx,2  ;除数
  	div bx    ;进行除法
  	 
  	 
  	push dx    ;余数入栈
   	cmp ax,0   ;比较商
 	ja _next   ;是否大于0,则继续除
 	 
print:
	pop ax 	    ;余数依次出栈,这里正好是倒着
	add al,48   ; al+48形成ascii码
	mov  ah , 0eh  ;打印
	int 10h
        loop print  ;cx寄存器一开始在除法的时候累加的

times 510 - ($ -$$) db 0
dw 0xaa55

image.png

如何在无操作系统情况下运行

上述代码我放在了下面地址。

https://github.com/houxinlin/nasm-examples/tree/main/mbr

这三个代码可以在无操作系统情况下运行,也非常简单,一个就两步。

首先编译

nasm 10to2.asm -o mbr.img

写入到u盘扇区

sudo dd if=demo.img of=/dev/sdb

然后设置bios启动方式为u盘,并设置为legacy方法。

重启后就可以看到效果。

但是对于windows的小伙伴,使用不了dd命令,如果真的想搞,可以下载HxD这个工具。