前言
这篇文章记录下汇编课程留下的三个作业,如下所示。
- 循环打印1-9
- 从键盘输入一个字符并根据其大写还是小写进行相反转换。
- 进制转换
在此之前,我们先说一下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,开头会写一些segment、end 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
下面就是效果图。
进制转换
到这里就需要考验水平了,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
如何在无操作系统情况下运行
上述代码我放在了下面地址。
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这个工具。