李忠汇编语言-初学-第九章详解
李忠汇编语言-第九章详解
文章目录
- 李忠汇编语言-第九章详解
- 前言
- 一、代码
- 二、运行
- 三、分析
- 1.昨天第八章详解没搞清楚的加载程序代码
- 2.今天第九章代码清单7-1的用户程序代码
- 四、总结
前言
之后都得这么搞了…
一、代码
用户代码如下:
;代码清单9-1;文件名:c09_1.asm;文件说明:用户程序 ;创建日期:2011-4-16 22:03;===============================================================================
SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00];用户程序入口点code_entry dw start ;偏移地址[0x04]dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4;段重定位表项个数[0x0a]realloc_begin:;段重定位表 code_segment dd section.code.start ;[0x0c]data_segment dd section.data.start ;[0x14]stack_segment dd section.stack.start ;[0x1c]header_end: ;===============================================================================
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
new_int_0x70:push axpush bxpush cxpush dxpush es.w0: mov al,0x0a ;阻断NMI。当然,通常是不必要的or al,0x80 out 0x70,alin al,0x71 ;读寄存器Atest al,0x80 ;测试第7位UIP jnz .w0 ;以上代码对于更新周期结束中断来说 ;是不必要的 xor al,alor al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(秒)push axmov al,2or al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(分)push axmov al,4or al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(时)push axmov al,0x0c ;寄存器C的索引。且开放NMI out 0x70,alin al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断;此处不考虑闹钟和周期性中断的情况 mov ax,0xb800mov es,axpop axcall bcd_to_asciimov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示mov [es:bx],ahmov [es:bx+2],al ;显示两位小时数字mov al,':'mov [es:bx+4],al ;显示分隔符':'not byte [es:bx+5] ;反转显示属性 pop axcall bcd_to_asciimov [es:bx+6],ahmov [es:bx+8],al ;显示两位分钟数字mov al,':'mov [es:bx+10],al ;显示分隔符':'not byte [es:bx+11] ;反转显示属性pop axcall bcd_to_asciimov [es:bx+12],ahmov [es:bx+14],al ;显示两位小时数字mov al,0x20 ;中断结束命令EOI out 0xa0,al ;向从片发送 out 0x20,al ;向主片发送 pop espop dxpop cxpop bxpop axiret;-------------------------------------------------------------------------------
bcd_to_ascii: ;BCD码转ASCII;输入:AL=bcd码;输出:AX=asciimov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位 add al,0x30 ;转换成ASCII shr ah,4 ;逻辑右移4位 and ah,0x0f add ah,0x30ret;-------------------------------------------------------------------------------
start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointermov ax,[data_segment]mov ds,axmov bx,init_msg ;显示初始信息 call put_stringmov bx,inst_msg ;显示安装信息 call put_stringmov al,0x70mov bl,4mul bl ;计算0x70号中断在IVT中的偏移mov bx,ax cli ;防止改动期间发生新的0x70号中断push esmov ax,0x0000mov es,axmov word [es:bx],new_int_0x70 ;偏移地址。mov word [es:bx+2],cs ;段地址pop esmov al,0x0b ;RTC寄存器Bor al,0x80 ;阻断NMI out 0x70,almov al,0x12 ;设置寄存器B,禁止周期性中断,开放更 out 0x71,al ;新结束后中断,BCD码,24小时制 mov al,0x0cout 0x70,alin al,0x71 ;读RTC寄存器C,复位未决的中断状态in al,0xa1 ;读8259从片的IMR寄存器 and al,0xfe ;清除bit 0(此位连接RTC)out 0xa1,al ;写回此寄存器 sti ;重新开放中断 mov bx,done_msg ;显示安装完成信息 call put_stringmov bx,tips_msg ;显示提示信息call put_stringmov cx,0xb800mov ds,cxmov byte [12*160 + 33*2],'@' ;屏幕第12行,35列.idle:hlt ;使CPU进入低功耗状态,直到用中断唤醒not byte [12*160 + 33*2+1] ;反转显示属性 jmp .idle;-------------------------------------------------------------------------------
put_string: ;显示串(0结尾)。;输入:DS:BX=串地址mov cl,[bx]or cl,cl ;cl=0 ?jz .exit ;是的,返回主程序 call put_charinc bx ;下一个字符 jmp put_string.exit:ret;-------------------------------------------------------------------------------
put_char: ;显示一个字符;输入:cl=字符asciipush axpush bxpush cxpush dxpush dspush es;以下取当前光标位置mov dx,0x3d4mov al,0x0eout dx,almov dx,0x3d5in al,dx ;高8位 mov ah,almov dx,0x3d4mov al,0x0fout dx,almov dx,0x3d5in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数cmp cl,0x0d ;回车符?jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ; mov bl,80 div blmul blmov bx,axjmp .set_cursor.put_0a:cmp cl,0x0a ;换行符?jnz .put_other ;不是,那就正常显示字符 add bx,80jmp .roll_screen.put_other: ;正常显示字符mov ax,0xb800mov es,axshl bx,1mov [es:bx],cl;以下将光标位置推进一个字符shr bx,1add bx,1.roll_screen:cmp bx,2000 ;光标超出屏幕?滚屏jl .set_cursormov ax,0xb800mov ds,axmov es,axcldmov si,0xa0mov di,0x00mov cx,1920rep movswmov bx,3840 ;清除屏幕最底一行mov cx,80.cls:mov word[es:bx],0x0720add bx,2loop .clsmov bx,1920.set_cursor:mov dx,0x3d4mov al,0x0eout dx,almov dx,0x3d5mov al,bhout dx,almov dx,0x3d4mov al,0x0fout dx,almov dx,0x3d5mov al,blout dx,alpop espop dspop dxpop cxpop bxpop axret;===============================================================================
SECTION data align=16 vstart=0init_msg db 'Starting...',0x0d,0x0a,0inst_msg db 'Installing a new interrupt 70H...',0done_msg db 'Done.',0x0d,0x0a,0tips_msg db 'Clock is now working.',0;===============================================================================
SECTION stack align=16 vstart=0resb 256
ss_pointer:;===============================================================================
SECTION program_trail
program_end:
演示用户程序代码如下:
;代码清单9-2;文件名:c09_2.asm;文件说明:用于演示BIOS中断的用户程序 ;创建日期:2012-3-28 20:35;===============================================================================
SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00];用户程序入口点code_entry dw start ;偏移地址[0x04]dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4;段重定位表项个数[0x0a]realloc_begin:;段重定位表 code_segment dd section.code.start ;[0x0c]data_segment dd section.data.start ;[0x14]stack_segment dd section.stack.start ;[0x1c]header_end: ;===============================================================================
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointermov ax,[data_segment]mov ds,axmov cx,msg_end-messagemov bx,message.putc:mov ah,0x0emov al,[bx]int 0x10inc bxloop .putc.reps:mov ah,0x00int 0x16mov ah,0x0emov bl,0x07int 0x10jmp .reps;===============================================================================
SECTION data align=16 vstart=0message db 'Hello, friend!',0x0d,0x0adb 'This simple procedure used to demonstrate 'db 'the BIOS interrupt.',0x0d,0x0adb 'Please press the keys on the keyboard ->'msg_end:;===============================================================================
SECTION stack align=16 vstart=0resb 256
ss_pointer:;===============================================================================
SECTION program_trail
program_end:
二、运行
运行如下:
代码清单-1
代码清单-2
三、分析
1.昨天第八章详解没搞清楚的加载程序代码
;代码清单8-1;文件名:c08_mbr.asm;文件说明:硬盘主引导扇区代码(加载程序) ;创建日期:2020-09-21 23:20;结束时间:2020-09-22 01:30;得来看下,加载程序时怎么搞得。;equ倒是知道,用来声明常数的,且常数的声明不会占用汇编地址,可以重复进行利用,且不占汇编地址。app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号);常数的声明不会占用汇编地址;哎 这里我用了好长时间 去做了实验才发现;用户程序起始逻辑扇区号 100 和 ;phy_base dd 0x10000 ;用户程序被加载的物理起始地址;上面这个没什么关系,上面定义为0x10000只是因为那个地方是空闲的,没有人用,所以可以把用户程序从;原本的逻辑扇区号100处移到哪里罢了 equ这里改成200 然后只要用户程序写入硬盘的时候,;逻辑扇区号为200就行 ;这个用户程序起始逻辑扇区号是告诉我们 要用fixvhdw2.exe 写入到;vhd虚拟硬盘文件的哪里和加载程序要到哪里去;找这个被我们写入的数据。 我脑子糊里糊涂的...;---------------------------------------------------------------------------------------
;主引导程序 16字节对齐 vstart=0x7c00
SECTION mbr align=16 vstart=0x7c00 ;因为该定义中有“vstart=0x7c00”子句,所以,它就不那么多余了。一旦有了该子句,;段内所有元素的汇编地址都将从 0x7c00 开始计算。否则,因为主引导程序的实际加载地址是;0x0000:0x7c00,当我们引用一个标号时,还得手工加上那个落差 0x7c00。;是这样呀!懂了懂了,之前在学王爽汇编语言的时候,课程设计二,虽然到现在,也没做会,但是搞那个标号处要加7C00H,搞了很长时间。;这里就简便多了, NASM的伪指令 vstart=0x7c00 接下来 所有的标号处地址什么的都是按照0x7c00计算的,完美呀!;设置堆栈段和栈指针 mov ax,0 mov ss,axmov sp,ax;设置栈倒是懂,写了很多遍。mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址 mov dx,[cs:phy_base+0x02];我在这花了好长时间,因为我想错了,脑子瓦特了, 一直在想那个程序的总大小怎么计算段地址什么的... mov bx,16 div bx mov ds,ax ;令DS和ES指向该段以进行操作mov es,ax ;因为放的是 10000 20位物理地址 然后用10000/16 DX存放的是高字部分 AX存放的是低字部分;然后 余数DX肯定为0 AX是商 也就是16位的段地址 ,我在想为什么不开始就用16位的段地址,不用非得在程序里面计算吧!;用于将该物理地址变成 16 位的段地址,并传送到 DS 和;ES 寄存器。因为该物理地址是 16 字节对齐的,直接右移 4 位即可。实际上,右移 4 位相当于除以;16(0x10),所以程序中的做法将这个 32 位物理地址(DX:AX)除以 16(在寄存器 BX 中),寄存;器 AX 中的商就是得到的段地址(在本程序中是 0x1000)。;以下读取程序的起始部分 xor di,dimov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 xor bx,bx ;加载到DS:0x0000处 call read_hard_disk_0;以下判断整个程序有多大mov dx,ds:[2] ;曾经把dx写成了ds,花了二十分钟排错 mov ax,ds:[0]; dx:ax 就是在用户程序里写的第一个里面的用户程序大小mov bx,512 ;512字节每扇区div bx;商是AX 余数是DXcmp dx,0;检测是否除尽;jnz 是不为0 没有除尽的意思 ,跳转到@1处jnz @1 ;未除尽,因此结果比实际扇区数少1 dec ax ;已经读了一个扇区,扇区总数减1 ;我在想什么叫已经读了一个字节,然后想想的确区读了@1:cmp ax,0 ;考虑实际长度小于等于512个字节的情况 ;jz是等于零 如果等于零就跳转到direct处jz direct;读取剩余的扇区push ds ;以下要用到并改变DS寄存器 mov cx,ax ;循环次数(剩余扇区数)@2:mov ax,ds;因为物理地址相差512个字节 但是在段地址的话 显现出来的是;假设 1:0 的物理地址为1*10H+0H=10H 下一个边界的物理地址是 (200H=512)10H+200H=210H 210H/10=21H 21H:0H=21H*10H+0H=210H;而 21H-1H=20H 我是笨脑子,用给出的答案,用我能理解的方式去理解。add ax,0x20 ;得到下一个以512字节为边界的段地址mov ds,ax xor bx,bx ;每次读时,偏移地址始终为0x0000 ;第一次的话 si是 100 那下一个逻辑扇区肯定是 si+1 就能得到的inc si ;下一个逻辑扇区 ;用 read_hard_disk_0 去读写call read_hard_disk_0loop @2 ;循环读,直到读完整个功能程序 pop ds ;恢复数据段基址到用户程序头部段 ;计算入口点代码段基址 direct:mov dx,[0x08]mov ax,[0x06]; dd section.code_1.start ;段地址[0x06] ;起始地址是0x06 dd 双字型 占四个字节 0x06 0x07 0x08 0x09 而在内存里面是低字端的 0x06 0x07放的是低字 0x08 0x09放的是高字call calc_segment_base;代码段地址知道了,然后呢?要回填,难道用户程序之后就用这个?我去看看用户程序,用户程序并没有,继续往下看mov [0x06],ax ;回填修正后的入口点代码段基址 ;开始处理段重定位表;原来如此 mov cx,[0x0a] ;需要重定位的项目数量mov bx,0x0c ;重定位表首地址;循环处理;段重定位表 的每个段的段地址数据 要改成在0x10000处的合理地址 喵呀 规范格式化 帅 realloc:mov dx,[bx+0x02] ;32位地址的高16位 mov ax,[bx]call calc_segment_base;得到16位段地址了;然后给原来的修改成现在的;这个回填的原因在用户程序里看到了 用户程序里的确用到这个了mov [bx],ax ;回填段的基址add bx,4 ;下一个重定位项(每项占4个字节) loop realloc ;这里就看到之前回填的原因了。;jmp far 关键字“far”的作用是告诉编译器,该指令应当编译成一个远转移。处理器执行这条指令后,;访问段寄存器 DS 所指向的数据段,从指令中给出的偏移地址处取出两个字,分别用来替代段寄存;器 CS 和指令指针寄存器 IP 的内容。;0x04 0x05 这个是偏移地址 一个字;0x06 0x07 这个是之前回填的段地址 ;然后加载器就会直接跳到代码段开始处进行执行代码,喵呀~jmp far [0x04] ;转移到用户程序 ;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区;输入:DI:SI=起始逻辑扇区号; DS:BX=目标缓冲区地址push axpush bxpush cxpush dxmov dx,0x1f2mov al,1out dx,al ;读取的扇区数inc dx ;0x1f3mov ax,siout dx,al ;LBA地址7~0inc dx ;0x1f4mov al,ahout dx,al ;LBA地址15~8inc dx ;0x1f5mov ax,diout dx,al ;LBA地址23~16inc dx ;0x1f6mov al,0xe0 ;LBA28模式,主盘or al,ah ;LBA地址27~24out dx,alinc dx ;0x1f7mov al,0x20 ;读命令out dx,al.waits:in al,dxand al,0x88cmp al,0x08jnz .waits ;不忙,且硬盘已准备好数据传输 mov cx,256 ;总共要读取的字数mov dx,0x1f0.readw:in ax,dxmov [bx],axadd bx,2loop .readwpop dxpop cxpop bxpop axret;-------------------------------------------------------------------------------
;这是计算16位段地址?对 在用户程序哪里 我们保存的是4字 32位的段地址
calc_segment_base: ;计算16位段地址;输入:DX:AX=32位物理地址;返回:AX=16位段基地址 ;dx是高字 ax是低字push dx ;adc 带进位加法 低的和低的加 然后高的用adc;那么为什么要加上 32位段地址 要加上 phy_base处的数据呢?因为我们把原先的数据复制到这里了呀!要先使它运行是不是就得在这里得到真正现在所在处的代码段地址add ax,[cs:phy_base]adc dx,[cs:phy_base+0x02];shr 是逻辑右移指令 ax是低字;尽管 DX:AX 中是 32 位的用户程序起始物理内存地址,理论上,它只有 20 位是有效的,低 16;位在寄存器 AX 中,高 4 位在寄存器 DX 的低 4 位。寄存器 AX 经右移后,高 4 位已经空出,只要将;DX 的最低 4 位挪到这里,就可以得到我们所需要的逻辑段地址。shr ax,4;很显然,代码清单 8-1 并不是这么做的,为的是演示另一个不同的指令 ror(第 142 行),也就;是循环右移(ROtate Right)。循环右移指令执行时,每右移一次,移出的比特既送;到标志寄存器的 CF 位,也送进左边空出的位。;假设 DX里面的数据是A000 B000 C000 1234 循环右移之后 1234 A000 B000 C000ror dx,4;and 逻辑与 0设0 1不变;0xfff0->1111 0000 0000 0000;假设DX->1234 A000 B000 C000;运行DX->1234 0000 0000 0000and dx,0xf000;or是逻辑或 1设1 0不变;假设ax->0000 AAAA AAAA AAAA;假设DX->1234 0000 0000 0000;运行AX->1234 AAAA AAAA AAAAor ax,dx;到此ax就是得到之后的16位的段地址pop dxret;-------------------------------------------------------------------------------phy_base dd 0x10000 ;用户程序被加载的物理起始地址times 510-($-$$) db 0db 0x55,0xaa
2.今天第九章代码清单7-1的用户程序代码
;代码清单9-1;文件名:c09_1.asm;文件说明:用户程序 ;创建日期:2020-09-21 22:20
;设置用户程序头部
;=========================================================================
SECTION header vstart=0 ;定义用户程序头部段 ;vstart=;段定义语句还可以包含“vstart=”子句。尽管定义了段,但是,引用某个标;号时,该标号处的汇编地址依然是从整个程序的开头计算的,而不是从段的开头处计算的。;那到底vstart是干什么的呢?;在我看来就是在汇编编译阶段,汇编编译器用来定义标明的段在下面没有或者有另外定义段的中间区域,所有标号等的汇编地址都从该语句定义处开始算起;如果没有该语句的定义,则默认从整个段头开始计算。;用户程序头部的段名定义好了;接下来就是程序总长度 ;用户程序的尺寸,即以字节为单位的大小。;这对加载器来说是很重要的,加载器需要根据这一信息来决定读取多少个逻辑扇区;如果要用加载器来加载用户程序的话,用户程序就必须设置用户程序的大小了。;我是这么想的,用一个标号来定义程序段尾部,然后把这个尾部当成段程序大小的标记。;事实作者也是这么写并且设定的。;program_length 程序总长度 program_end标号果然放在了程序的最后;那为什么要用dd 来定义一个双字节呢?用户程序可能很大,16 位的长度不足以表示 65535 以上的数值。;原来是这样,留好足够的缓冲,避免很多问题,并且这段代码,重复使用性很高。;但是,这个program_end标号不能就这么写,得让它定义在一个段内,一是格式方便,二是简洁一目了然,毕竟,下面还有很多段。program_length dd program_end ;程序总长度[0x00] ;那接下来干什么?其实我忘了,虽然是昨天才学的,嗯。但依稀记得,应该是什么表来着。;② 应用程序的入口点,包括段地址和偏移地址。加载器并不清楚用户程序的分段情况,;更不知道第一条要执行的指令在用户程序中的位置。因此,必须在头部给出第一条指令的段地;址和偏移地址,这就是所谓的应用程序入口点(Entry Point)。;应用程序入口点!哦,跟加载器做了一个双方知道的协议,你必须在程序的开头,要有程序的大小,这样我才能把你全部的数据加载到里面进行处理。;既然,程序全部都到我的里面了,那我还需要什么呢?我得知道你的门在哪里,我怎么用你。;用户程序入口点code_entry dw start ;偏移地址[0x04]dd section.code.start ;段地址[0x06] ;理想情况下,当用户程序开始运行时,执行的第一条指令是其代码段内的第一条指令。换;句话说,入口点位于其代码段内偏移地址为 0 的地方。但是,情况并非总是如此。尤其是,很;多程序并非只有一个代码段,比如本章源代码清单 8-2 就包含了两个代码段。所以,需要在用;户程序头部明确给出用户程序在刚开始运行时,第一条指令的位置,也就是第一条指令在用户;程序代码段内的偏移地址。;好吧!得先知道第一条指令处,然后,去执行它....;那么段地址就是code的起始地址,code是16字节对齐的,刚好可以使用。;为了方便取得该段的汇编地址,NASM 编译器提供了以下的表达式,可以用在你的程序中:section.段名称.start;获取段的汇编地址就是这样 毕竟段不可能变成标号来获取,懂一点点了。;然后就是段列表什么的了。realloc_tbl_len dw (header_end-realloc_begin)/4;段重定位表项个数[0x0a]realloc_begin:;段重定位表 code_segment dd section.code.start ;[0x0c]data_segment dd section.data.start ;[0x14]stack_segment dd section.stack.start ;[0x1c];③ 段重定位表。用户程序可能包含不止一个段,比较大的程序可能会包含多个代码段和多个;数据段。这些段如何使用,是用户程序自己的事,但前提是程序加载到内存后,每个段的地址必须;重新确定一下。;段的重定位是加载器的工作,它需要知道每个段在用户程序内的位置,即它们分别位于用户程;序内的多少字节处。为此,需要在用户程序头部建立一张段重定位表。;用户程序可以定义的段在数量上是不确定的,因此,段重定位表的大小,或者说表项数是不确;定的。为此,代码清单 8-2 第 14 行,声明并初始化了段重定位表的项目数。因为段重定位表位于;两个标号 header_end 和 code_1_segment 之间,而且每个表项占用 4 字节,故实际的表项数为;(header_end – code_1_segment) / 4;这个值是在程序编译阶段计算的,先用两个标号所代表的汇编地址相减,再除以每个表项的长度 4。;紧接着表项数的,是实际的段重定位表,每个表项用伪指令 dd 声明并初始化为 1 个双字。代;码清单 8-2 一共定义了 5 个段,所以这里有 5 个表项,依次计算段开始汇编地址的表达式并进行初;始化。;有点迷糊,我以为这个是固定的,原来是我狭隘的,这个并不是固定的。;先有 段重定位表项个数 然后再有段重定义表 这大概理解了。; (header_end-realloc_begin)/4 这是个什么意思?哦,一个简单的计算,就能重复的利用了,要是设置新的用户程序,不用更改这里的代码;直接
header_end:;用户程序头部的尾部 这个是用来当 计算段重定位表项个数时的尾部,用 (realloc_begin - header_end )realloc_begin ;标号记录了段重定义表的起始地址 然后 header_end 刚好是在 段重定位表 的结尾 两个一减就是段重定位表的字节大小;而每个表项占用 4 字节 只要除以4就能得到 段重定位表项个数了 秒呀;段重定义表就这样完成了。而且每个表项前有专门的标号记录,我想大概是在后续的时候,用这些段的时候,可以方便快捷的找到吧!
;===============================================================================
;这里就是代码段了。
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐) ; align=; 在段定义中使用“align=”子句,用于指定某个 SECTION 的汇编地址对齐方式。比如说,“align=16”就表示段; 是 16 字节对齐的,“align=32”就表示段是 32 字节对齐的。; 在源程序编译阶段,编译器将根据 align 子句确定段的起始汇编地址。;表明段的起始汇编地址?似懂非懂。对了是来这里找标号start的
new_int_0x70:push axpush bxpush cxpush dxpush es.w0: mov al,0x0a ;阻断NMI。当然,通常是不必要的or al,0x80 out 0x70,alin al,0x71 ;读寄存器Atest al,0x80 ;测试第7位UIP jnz .w0 ;以上代码对于更新周期结束中断来说 ;是不必要的 xor al,alor al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(秒)push axmov al,2or al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(分)push axmov al,4or al,0x80out 0x70,alin al,0x71 ;读RTC当前时间(时)push axmov al,0x0c ;寄存器C的索引。且开放NMI out 0x70,alin al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断;此处不考虑闹钟和周期性中断的情况 mov ax,0xb800mov es,axpop axcall bcd_to_asciimov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示mov [es:bx],ahmov [es:bx+2],al ;显示两位小时数字mov al,':'mov [es:bx+4],al ;显示分隔符':'not byte [es:bx+5] ;反转显示属性 pop axcall bcd_to_asciimov [es:bx+6],ahmov [es:bx+8],al ;显示两位分钟数字mov al,':'mov [es:bx+10],al ;显示分隔符':'not byte [es:bx+11] ;反转显示属性pop axcall bcd_to_asciimov [es:bx+12],ahmov [es:bx+14],al ;显示两位小时数字mov al,0x20 ;中断结束命令EOI out 0xa0,al ;向从片发送 out 0x20,al ;向主片发送 pop espop dxpop cxpop bxpop axiret;-------------------------------------------------------------------------------
bcd_to_ascii: ;BCD码转ASCII;输入:AL=bcd码;输出:AX=asciimov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位 add al,0x30 ;转换成ASCII shr ah,4 ;逻辑右移4位 and ah,0x0f add ah,0x30ret;-------------------------------------------------------------------------------
;用户程序运行指令处 在用户程序头部 定义的用户程序入口点的偏移地址 既然有了偏移地址那就得有段地址,既然是在code段了。
;刚做完加载程序源码的分析,这里start 是直接从加载程序哪里跳过来的首地址 这个用户程序执行的第一条代码也就是这里。
;逻辑逻辑 现在真正的发觉编程是个逻辑思维的活儿 酷!!!!
start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointer;这里设置栈 懂 stack_segment 而且这个stack_segment 已经被加载程序修改成当前0x10000处的段地址了 哈哈 什么都不用修改 在这里 直接使用这个就好了mov ax,[data_segment]mov ds,ax;设置数据段也是同样mov bx,init_msg ;显示初始信息;bx是数据段里面数据的首地址;去看看 put_string 这个是干什么用的?call put_string;根据情况显示东西 原来如此 这里显示要安装int7Hmov bx,inst_msg ;显示安装信息 call put_string;根据中断类型码找到在中断向量表里面中断程序的位置mov al,0x70mov bl,4;就是0x70在中断向量表里的偏移地址mul bl ;计算0x70号中断在IVT中的偏移;ax就是得到的偏移地址mov bx,ax;要修改中断向量表了 为了避免出现一些问题 最好先清中断 ;设置好之后 再恢复 cli ;防止改动期间发生新的0x70号中断push esmov ax,0x0000mov es,ax;把0x70处改成我们设置的新的0x70中断例程地址mov word [es:bx],new_int_0x70 ;偏移地址。mov word [es:bx+2],cs ;段地址pop es;设置RCT的工作状态 使它能够产生中断信号给8259中断控制器mov al,0x0b ;RTC寄存器Bor al,0x80 ;阻断NMI out 0x70,almov al,0x12 ;设置寄存器B,禁止周期性中断,开放更 out 0x71,al ;新结束后中断,BCD码,24小时制 mov al,0x0cout 0x70,alin al,0x71 ;读RTC寄存器C,复位未决的中断状态in al,0xa1 ;读8259从片的IMR寄存器 and al,0xfe ;清除bit 0(此位连接RTC)out 0xa1,al ;写回此寄存器 sti ;重新开放中断 mov bx,done_msg ;显示安装完成信息 call put_stringmov bx,tips_msg ;显示提示信息call put_stringmov cx,0xb800mov ds,cxmov byte [12*160 + 33*2],'@' ;屏幕第12行,35列.idle:hlt ;使CPU进入低功耗状态,直到用中断唤醒not byte [12*160 + 33*2+1] ;反转显示属性 jmp .idle;-------------------------------------------------------------------------------
;显示字符串的???原来如此 还有检测字符串
put_string: ;显示串(0结尾)。;输入:DS:BX=串地址mov cl,[bx]or cl,cl ;cl=0 ?;用这种方法来检测是不是到了字符串尾可以呀 也可以用 cmp cl,cl;jz 是 为0跳转 如果为0就说明 没有字符要显示了 直接跳转出去jz .exit ;是的,返回主程序 call put_charinc bx ;下一个字符 jmp put_string.exit:ret;-------------------------------------------------------------------------------
;显示字符串 以下就是作者写的显示可打印字符串了 之前第八章看到过
put_char: ;显示一个字符;输入:cl=字符asciipush axpush bxpush cxpush dxpush dspush es;需要知道的东西;索引寄存器的端口号是0x3d4,可以向它写入一个值,;用来指定内部的某个寄存器。比如,;两个 8 位的光标寄存器,其索引值分别是 14(0x0e);和 15(0x0f),分别用于提供光标位置的;高 8 位和低 8 位。;以下取当前光标位置mov dx,0x3d4mov al,0x0eout dx,al;通过索引端口告诉显卡,现在要操作 0x0e 号寄存器。mov dx,0x3d5in al,dx ;高8位 ;al 是要读取的高8位数据 但是我们要完整的 接下来还有读取低八位的;这里把读取到的al给ah 然后再读取低8位 读取完刚好 ah高8位 al低八位 ;ax刚好是完整的光标位置数据mov ah,al;通过数据端口从 0x0e 号端口读出 1 字节的数据,并传送到寄存器 AH 中,;这是屏幕光标位置的高 8 位。mov dx,0x3d4mov al,0x0fout dx,almov dx,0x3d5in al,dx ;低8位 ;从 0x0f 号寄存器读出光标位置的低 8 位。现在,寄存器 AX 中;是完整的光标位置数据。;接下来要用ax数据吗?mov bx,ax ;BX=代表光标位置的16位数cmp cl,0x0d ;回车符?jnz .put_0a ;不是。看看是不是换行等字符 ;下面到 .put_0a 之前是处理回车符的 ;之前没看清 回车符 是回到该行的行首 我一直一直回车就是换行;之前第八章也没看清.... 尴尬;ax里面就是光标位置数据 就是当前光标在显示器的位置 ;假设 AX是160的话 ;每行80个字节;只要用160/80 是不是就能得到现在显示到第几行了mov ax,bx ; mov bl,80 div bl;ax 是商 2 dx是余数 0;再用得到的行号乘以80 就得到了当前行 行首的光标值 ;dl8位 那么用的就是al了;不用将dx清零 因为不用它mul blmov bx,axjmp .set_cursor.put_0a:cmp cl,0x0a ;换行符?jnz .put_other ;不是,那就正常显示字符 ;bx是光标位置数据 +80就是下一行add bx,80jmp .roll_screen.put_other: ;正常显示字符mov ax,0xb800mov es,ax;shl 是左移1位 乘以2 来得到该位置(字符)在显存中的偏移地址;对呀 光标位置是 0~1999 2000个字符 还是多少来着 ;因为1个光标所显示的是一个字符;而我们要得到该字符在显存中的偏移地址不就得 乘以2吗?喵呀!shl bx,1mov [es:bx],cl;以下将光标位置推进一个字符;之前为了得到偏移地址 乘以2 现在用shr 右移一位就除以2了shr bx,1;然后得到下一个要显示的光标位置add bx,1.roll_screen:cmp bx,2000 ;光标超出屏幕?滚屏;jl 是小于 如果光标是2000说明到下一页了你就得滚屏了 第一行没了;第二行变第一行 最后一行清空jl .set_cursor;下面处理滚屏的mov ax,0xb800mov ds,axmov es,ax;设置数据的来源和取出 因为操作的都是显存里的内容 所以都是0xb800;设置数据传送方向 cld 正向传送cld;设置数据传送地址;si 是数据来源处 di是数据去向处;si=0xa0=160 1行80个字符,160就是第二行第一列的位置 ;80*2=160 mov si,0xa0;di=0x00=0第一行第一列的位置mov di,0x00;因为最后一行不用管 最后一行移到倒数第二行后 ;为了滚屏的效果把最后一行清空就行了mov cx,1920;按照字开始传送1920次;从第2行开始到最后一行 传到第一行 到 倒数第二行处 最后一行清空rep movswmov bx,3840 ;清除屏幕最底一行mov cx,80.cls:;0x720->0000 0111 0010 0000;内存中是 0010 0000 0000 0111;0010 0000是字符的ASCII码 20H为显示空;0000 0111是字符的字符属性 背景颜色为0000 为黑色;前景颜色 0111 是白色 但是没有字符所以什么都不显示 还是黑色mov word[es:bx],0x0720add bx,2loop .clsmov bx,1920.set_cursor:;接下来就是把最后的光标位置写入到光标寄存器里面去。;要进行操作mov dx,0x3d4mov al,0x0eout dx,al;要进行高字节写入mov dx,0x3d5mov al,bhout dx,al;要进行操作mov dx,0x3d4mov al,0x0fout dx,al;要进行低字节写入mov dx,0x3d5mov al,blout dx,alpop espop dspop dxpop cxpop bxpop axret ;===============================================================================
;这里是数据段了 十六字节对齐的 下面的标号处的汇编地址都是从该段开始算起,从0
SECTION data align=16 vstart=0init_msg db 'Starting...',0x0d,0x0a,0inst_msg db 'Installing a new interrupt 70H...',0done_msg db 'Done.',0x0d,0x0a,0tips_msg db 'Clock is now working.',0;===============================================================================
;这里是栈段了 十六字节对齐的 下面的标号处的汇编地址都是从该段开始算起,从0
;
SECTION stack align=16 vstart=0resb 256; resb; 伪指令; 意思是从当前位置开始,保留指定数量的字节,但不初始; 化它们的值。在源程序编译时,编译器会保留一段内存区域,用来存放编译后的内容。当它看; 到这条伪指令时,它仅仅是跳过指定数量的字节,而不管里面的原始内容是什么。内存是反复; 使用的,谁也无法知道以前的使用者在这里留下了什么。也就是说,跳过的这段空间,每个字; 节的值是不确定的。; 用来声明未初始化数据的指令。;这个就是类似于 db dup 256(0) 大概这样的操作吧!;ss_pointer 这个是干什么的?去翻了下代码是用来进行设置SP的,哦,既然是vstart=0 ss_pointer 标号就是 栈的大小了, 可以用来当SP;而且如果想修改栈的大小,只需要在上面resb 修改就行,不用去到处找mov sp,XXX 修改。原来如此 ss_pointer:
;===============================================================================
;这就是定义了一个程序尾部段了 这里没有用vstart= 的原因是 我们用 program_end ;来记录程序的大小,那就的必须起始于程序段的开始,而如果设置了,那就的必须起始于程序段的开始,而如果设置了,那
;就对我们没有任何用处。
SECTION program_trail
program_end: ;程序的长度取自程序中的一个标号“program_end”,这是允许的。在编译阶段,编译器将该标
;号所代表的汇编地址填写在这里。该标号位于整个源程序的最后,从属于段“trail”。由于该段并没
;有 vstart 子句,所以,标号“program_end”所代表的汇编地址是从整个程序的开头计算的。换句话
;说,program_end 所代表的汇编地址,在数值上等于整个程序的长度。
四、总结
加油、漏油,卖油,爱老虎油。
明天第九章这个关于new_int7还得再看一遍。
李忠汇编语言-初学-第九章详解相关推荐
- 李忠汇编语言-初学-第八章详解
李忠汇编语言-第八章详解 文章目录 李忠汇编语言-第八章详解 前言 一.代码 二.运行 三.分析 总结 前言 还是得对源码和对应的书籍内容进行逐行逐字解析 提示:以下是本篇文章正文内容,下面案例可供参 ...
- 李忠汇编语言-初学-第七天
学习目标 第九章 学习小结 有点理解第八章和第九章内容了,但是还不是完全懂,明天继续吧! 学习内容 类型:汇编语言 进度:初学 天数:第七天 日期:2020年09月21日 书籍:李忠汇编语言 相关:李 ...
- 李忠汇编语言-初学-第十二天
学习目标 第十三章 学习小结 头越来越大了. 学习内容 类型:汇编语言 进度:初学 天数:第十二天 日期:2020年09月26日 书籍:李忠汇编语言 相关:无 内容:李忠汇编语言 第十三章 时间:20 ...
- 李忠汇编语言-初学-第六天
学习目标 第八章 学习小结 学习可真好玩,熬夜,万岁!!!! 学习内容 类型:汇编语言 进度:初学 天数:第六天 日期:2020年09月20日 书籍:李忠汇编语言 相关:李忠汇编语言网易云课堂视频 内 ...
- 李忠汇编语言-初学-第二天
学习目标 李忠汇编语言-第二章.第三章 学习内容 类型:汇编语言 进度:初学 天数:第二天 日期:2020年9月16日 书籍:李忠汇编语言 相关:李忠汇编语言哔哩哔哩相关教学视频 内容:书籍第二章.第 ...
- 李忠汇编语言-初学-第三天
学习目标 书籍第四章.第五章 学习内容 类型:汇编语言 进度:初学 天数:第三天 日期:2020年9月17日 书籍:李忠汇编语言 相关:李忠汇编语言哔哩哔哩视频 内容:书籍第四章.第五章 视频47-5 ...
- 李忠汇编语言-初学-第九天
学习目标 第十一章 学习小结 头巨疼,哪怕这几天都在划水,可作息属实不正常,明天调整一天,顺便总结归纳复习第7.8.9.10.11章内容,不是很扎实. 也不知道是换季还是什么原因,巨疼,我得先睡了. ...
- 李忠汇编语言-初学-第八天
学习目标 第十章 学习小结 还是不够扎实,继续码吧!!! 学习内容 类型:汇编语言 进度:初学 天数:第八天 日期:2020年09月22日 书籍:李忠汇编语言 相关:李忠汇编语言网易云课堂视频 内容: ...
- 李忠汇编语言-初学-第四天
学习目标 书籍 第六章 学习内容 类型:汇编语言 进度:初学 天数:第四天 日期:2020年09月18日 书籍:李忠汇编语言 相关:李忠汇编语言网易云课堂视频 内容:书籍 第六章 视频86 时间:20 ...
- 李忠汇编语言-初学-第五天
学习目标 书籍第七章 学习小结 之前游刃有余,现在呵呵,看完了第八章,也只是大概总结了下,明天逮着源码死薅. 主要是身体不舒服,难受的很.不知道还能抗多少天. ## 学习内容 类型:汇编语言 进度:初 ...
最新文章
- TypeScript 从听说到入门(上篇)
- php7判断邮箱格式是否正确,利用php实现验证邮箱格式是否正确
- python中knn_如何在python中从头开始构建knn
- SyntaxError: Non-UTF-8 code starting with ‘\xe2‘ 今天是小白上线的一天
- 基于JAVA+SpringMVC+Mybatis+MYSQL的员工事物管理系统
- kubernetest pod为ContainerCreating、ImagePullBackOff状态 怎么办
- creo不完全约束_Creo绘图1:1输出AutoCAD配置方法详解,工程图输出再不用担心尺寸乱变!...
- 一个故事讲完CPU的工作原理
- PASCAL标准过程与函数
- 0723数组复习 堆区 动态数组
- 计算机按键截图,截图按什么键,电脑按哪个键是截屏
- hexo之icarus主题的美化修改以及简单的SEO配置
- java poi word换行符_poi读取word的换行符问题
- 如何输入“·”间隔号
- qq音乐会员联合会员都有哪些
- 行人reid ,多个开源地址数据变成 market1501格式合并,附代码,数据地址
- MAX7219产品级驱动分享
- C++的继承(一): 让蟋蟀继承蚱蜢
- Springboot+Mybatis-plus实现增删改查功能超详细
- 鸿蒙App文件结构说明