others linux服务器运维 django3 监控 k8s golang 数据库 大数据 前端 devops 理论基础 java oracle 运维日志

简单操作系统内核开发1

访问量:1560 创建时间:2021-01-08

汇编

在屏幕上显示数字

数字1在内存中的二进制表示:000000001 (0x01) 字符1在内存中的二进制表示形式: 00110001 (0x31)

在屏幕上显示125,需要在屏幕上显示3个数字字符(向显存写入这3个字符的编码和显示属性) '1','2','5',先找到它们对应的数字的编码

1->0000 0001 2->0000 0010 5->0000 0101

第二步,数字0-9对应的数字字符编码是0x30-0x39,将上面数字的编码加上0x30,就得到了对应的数字字符编码。

无符号整数的除法指令:div,只需要一个操作数,除数所在的寄存器或内存地址。被除数的位置由除数决定:

如果在指令中指定的是8位寄存器或者8位操作数的内存地址,则意味着被除数在寄存器AX里,相除后,商在寄存器AL里,余数在寄存器AH里。 div bh div byte [0x3030]

如果在指令中指定的是16位寄存器或者16位的操作数内存地址,则被除数是32位的,低16位在寄存器AX里,高16位在寄存器DX里,相除后,商在寄存器AX里,余数在寄存器DX里。 div bx div word [0x3030]

80386 32位处理器除法: 如果在指令中指定的是32位的寄存器或者32位的操作数内存地址,则被除数是64位的,低32位在寄存器exa里;高32位在寄存器edx里。相除后,商在寄存器eax里,余数在寄存器edx. div ebx div dword [0x3030]

64位处理器除法: 如果在指令中指定的是64位寄存器或者64位的操作数内存地址,则被除数是128位,低64位在寄存器rax中,高64位在寄存器rdx中。相除后商在寄存器rax中,余数在寄存器rdx中。 div rbx div qword [0x3030]

start:
    ; 378除以37的主引导扇区代码
    mov ax,378
    mov bl,37
    div bl ; AL=商10,AH=余数8
current:
    times 510-(current-start) db 0
    db 0x55,0xaa

xor指令:

start:
    ; 65535除以10的主引导扇区代码
    mov ax,65535
    mov dx,0
    mov bx,10
    div bx ; AX=商6553,DX=余数5
current:
    times 510-(current-start) db 0
    db 0x55,0xaa
start:
    ; 65535除以10的主引导扇区代码因为65535除以10是6553,大于8为寄存器的存储范围,要用16位寄存器保存结果
    mov ax,65535
    xor dx,dx ;自己和自己异或,dx最终所有位都为0, 等价于 mov dx,0  但是2个操作数都是寄存器比mov dx,0快
    mov bx,10
    div bx ; AX=商6553,DX=余数5
current:
    times 510-(current-start) db 0
    db 0x55,0xaa

xor 指令,异或 eXclusive or ;2个输入相同,输出结果是0,两个输入不相同输出结果是1; 有2个操作数xor r/m ,r/m/imm ;左操作数可以使寄存器r或者内存地址m,右操作数可以是寄存器r/内存地址m/或者立即数imm,异或操作的结果保存在左操作数中r/m,2个操作数长度相同 0 :fa-plus-circle: 0 = 0 0 :fa-plus-circle: 1 = 1 1 :fa-plus-circle: 1 = 0 1 :fa-plus-circle: 0 = 1 129 :fa-plus-circle: 127 = 254 10000001 :fa-plus-circle: 01111111 = 11111110

xor bh ,al
xor dx ,cx
xor ax,4
xor word [0x3030],67
xor si,[0x3030]

add 指令:add r/m,r/m/imm 需要2个操作数,左操作数可以使寄存器r或者内存地址m,右操作符可以使寄存器r/内存地址m/立即数imm;相加后保存在左侧r或者m中,2个操作数不能同时为m内存地址

example:(在32和64位系统中add指令可以使用32位和64位操作数)

add bh,al
add dx,cx
add ax,3
add word [0x3030] ,67 ;左操作数word 16位
add si, [0x3030]
start:
    ; 在屏幕上显示数字65535
    mov ax,65535
    xor dx,dx ;dx最终所有位都为0, 等价于 mov dx,0  
    mov bx,10
    div bx ; AX=商6553,DX=余数5
    add dl,0x30 ;余数5在dx的低地址位dl中,将dl加上0x30就是数字字符的编码
    mov cx,0   ;设置段地址ds 0000,用于段地址+buffer寻找buffer的地址
    mov dx,cs  
    mov [0x7c00+buffer],dl ;将dl编码传到0x7c00+buffer的内存地址处。
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+1],dl ;将3放入buffer第二个字节
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+2],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+3],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+4],dl 
buffer db 0,0,0,0,0  ;用位指令db开辟5个字节空间,db开辟的内个内存单位都是字节,初始值是0,
current:
    times 510-(current-start) db 0
    db 0x55,0xaa
;这里的buffer也可以使用current代替,效果一样

在程序中保留内存空间,使用标号访问这些内存空间。(65535通过除法依次分解的是53556,按这个顺序在屏幕显示是倒序,必须先保存结果,再显示。)

db 0,0,0,0,0 ;用位指令db开辟5个字节空间,db开辟的内个内存单位都是字节,初始值是0, 需要用标号buffer 来对空间第一个字节的起始地址进行标记,用于寻址,buffer后的冒号可以省略。 (编程时,程序的起始地址是0,未运行,运行后程序会被加载再一定的位置,这个位置不是0)

段超越前缀: 使用段超越前缀在2个数据段之间协同工作。

start:
    ; 在屏幕上显示数字65535
    mov ax,65535
    xor dx,dx ;dx最终所有位都为0, 等价于 mov dx,0  
    mov bx,10
    div bx ; AX=商6553,DX=余数5
    add dl,0x30 ;余数5在dx的低地址位dl中,将dl加上0x30就是数字字符的编码
    mov cx,0   ;设置段地址ds 0000,用于段地址+buffer寻找buffer的地址
    mov dx,cs  
    mov [0x7c00+buffer],dl ;将dl编码传到0x7c00+buffer的内存地址处。
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+1],dl ;将3放入buffer第二个字节
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+2],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+3],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+4],dl 

    mov al, [0x7c00+buffer+4]
    mov cx,0xb800  ;因为ds不能直接修改,通过cx间接设置段偏移地址ds为0xb800
    mov ds,cx 
    mov [0x00],al ;将al的值传给0xb800:0x00显存处
    mov byte [0x01],0x2f; 设置显存属性,立即数与内存地址不能知道数据大小位数,需要byte指定
    mov cx,0 ;因为ds从0变到0xb800,导致无法用[0x7c00+buffer+3]定位字符所在的位置了,需要将ds设置为零
    mov ds,cx

    mov al,[0x7c00+buffer+3]
    mov cx,0xb800  ;设置段偏移地址ds为0xb800
    mov ds,cx 

    mov [0x02],al ;
    mov byte [0x03],0x2f;

    ;下面的设置省略############################

buffer db 0,0,0,0,0  
current:
    times 510-(current-start) db 0
    db 0x55,0xaa

通过2个寄存器实现:

start:
    ; 在屏幕上显示数字65535
    mov ax,65535
    xor dx,dx ;dx最终所有位都为0, 等价于 mov dx,0  
    mov bx,10
    div bx ; AX=商6553,DX=余数5
    add dl,0x30 ;余数5在dx的低地址位dl中,将dl加上0x30就是数字字符的编码
    mov cx,0   ;设置段地址ds 0000,用于段地址+buffer寻找buffer的地址
    mov dx,cs  
    mov [0x7c00+buffer],dl ;将dl编码传到0x7c00+buffer的内存地址处。
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+1],dl ;将3放入buffer第二个字节
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+2],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+3],dl 
    xor dx,dx
    div bx
    add dl,0x30
    mov [0x7c00+buffer+4],dl 

    mov cx,0xb800 ;前面用ds来回切换很麻烦,这里用es存储0xb800段(这样ds就不用动了0x0000)
    mov es,cx

    mov al,[0x7c00+buffer+4]
    mov [es:0x00],al ;偏移地址0x00之前的 es: 叫做段超越前缀,没有段超越前缀时默认的段地址在ds中
    mov byte [es:0x01],0x2f  ;设置颜色 
    mov al,[0x7c00+buffer+3]
    mov [es:0x02],al 
    mov byte [es:0x03],0x2f  
    mov al,[0x7c00+buffer+2]
    mov [es:0x04],al 
    mov byte [es:0x05],0x2f  
    mov al,[0x7c00+buffer+1]
    mov [es:0x06],al 
    mov byte [es:0x07],0x2f  
    mov al,[0x7c00+buffer]
    mov [es:0x08],al 
    mov byte [es:0x09],0x2f  

again:
    jmp again   ;跳转到自己
buffer db 0,0,0,0,0  
current:
    times 510-(current-start) db 0
    db 0x55,0xaa

实验:

         mov ax,0xb800                 ;指向文本模式的显示缓冲区
         mov es,ax

         ;以下显示字符串"Label offset:"
         mov byte [es:0x00],'L'
         mov byte [es:0x01],0x07
         mov byte [es:0x02],'a'
         mov byte [es:0x03],0x07
         mov byte [es:0x04],'b'
         mov byte [es:0x05],0x07
         mov byte [es:0x06],'e'
         mov byte [es:0x07],0x07
         mov byte [es:0x08],'l'
         mov byte [es:0x09],0x07
         mov byte [es:0x0a],' '
         mov byte [es:0x0b],0x07
         mov byte [es:0x0c],"o"
         mov byte [es:0x0d],0x07
         mov byte [es:0x0e],'f'
         mov byte [es:0x0f],0x07
         mov byte [es:0x10],'f'
         mov byte [es:0x11],0x07
         mov byte [es:0x12],'s'
         mov byte [es:0x13],0x07
         mov byte [es:0x14],'e'
         mov byte [es:0x15],0x07
         mov byte [es:0x16],'t'
         mov byte [es:0x17],0x07
         mov byte [es:0x18],':'
         mov byte [es:0x19],0x07

         mov ax,number                 ;取得标号number的汇编地址,程序编写时的地址
         mov bx,10  ; 除数10

         ;设置数据段的基地址 ds = cs,cs 是程序加载到内存地址开始位置,这里应该是是0x0000,只是为了演示从cs复制到ds的过程,
         mov cx,cs
         mov ds,cx

         ;求个位上的数字
         mov dx,0 ; dx与ax 组成32位的被除数
         div bx  ;除10
         mov [0x7c00+number+0x00],dl   ;保存个位上的数字

         ;求十位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x01],dl   ;保存十位上的数字

         ;求百位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x02],dl   ;保存百位上的数字

         ;求千位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x03],dl   ;保存千位上的数字

         ;求万位上的数字 
         xor dx,dx
         div bx
         mov [0x7c00+number+0x04],dl   ;保存万位上的数字
         ;上面因为ax最大值是65535,所以分解了5次

         ;以下用十进制显示标号的偏移地址
         mov al,[0x7c00+number+0x04]
         add al,0x30
         mov [es:0x1a],al
         mov byte [es:0x1b],0x04

         mov al,[0x7c00+number+0x03]
         add al,0x30
         mov [es:0x1c],al
         mov byte [es:0x1d],0x04

         mov al,[0x7c00+number+0x02]
         add al,0x30
         mov [es:0x1e],al
         mov byte [es:0x1f],0x04

         mov al,[0x7c00+number+0x01]
         add al,0x30
         mov [es:0x20],al
         mov byte [es:0x21],0x04

         mov al,[0x7c00+number+0x00]
         add al,0x30
         mov [es:0x22],al
         mov byte [es:0x23],0x04

         mov byte [es:0x24],'D'  ;用D说明屏幕显示的数是十进制
         mov byte [es:0x25],0x07

   infi: jmp near infi                 ;无限循环,不用near生成2字节指令,near生成3字节跳转指令

  number db 0,0,0,0,0

  times 203 db 0
            db 0x55,0xaa

总结

汇编地址:编写程序时,代码所在处的地址(不是程序执行时真实的物理偏移地址) 偏移地址: 段地址+偏移地址用于定位物理地址 标号: 在nasm中标号可以由字符 数字 _ $ # @ ~ . ?组成,以字母 . _ ?开头,标号后面可以跟一个冒号, 冒号不是必须的。标号的作用是定位指令或者数据,在编写程序时标号代表指令或者数据的汇编地址(在程序运行时程序使用段地址和段内偏移地址),程序在运行前需要加载到某个内存段,如果程序从段的起始处(xxxx:0000)加载, 偏移地址就等于汇编地址。如果程序加载地址不是起始处(0000:7c00是加载处,段的起始地址是0000:0000,偏移地址为7c00+汇编地址,物理地址为0000:7c00+标号代表的汇编地址)

数据长度: 在需要两个操作数的指令中,如果至少一个是寄存器。则不需要长度修饰符(mov ah, bl ; mov [buffer],ax ; xor byte [buffer],0x55)。如果只有1个操作数且不是寄存器,必须使用长度修饰符 (div word [divisor]) - 伪指令:

db 用来定义8位长度-字节数据: db 0x55 dw 定义16位数据-字数据 : dw 0x55aa dd 定义32位双字数据: dd 0xabcd1234 dq 定义四字 64位长度数据: dq 0x12345678aabbccdd

伪指令 times 用来重复后面的指令若干次: times 5 mov ax,bx 等价于mov ax,bx 执行5次;times 50 dw 0 等价于dw 0 五十次。

循环、批量传送和条件转移

当数据和指令混合时如何避免执行到这些非指令的数据。

mov byte [0x00],'L'
mov byte [0x01],0x2f

上面这种字符和指令在一起的方式很笨拙。如果要显示的内容很多需要大量的指令罗列重复,修改也非常复杂。 通过定义一个数据区,将文本内容与显示指令分离。当要显示时,通过指令逐个取出并显示。如下所示:

  jmp near start ;程序被加载到7c00处,需要直接跳到代码处执行,而不是执行下面的mytext数据区,mytext如果放在程序尾部,可以不用jmp跳转

  mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\  ; \是续行符,表示和下一行是一行内容,mytext标号用于后期引用
            'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
  number db 0,0,0,0,0
  start:
         mov ax,0x7c0                  ;设置数据段基地址 
         mov ds,ax

设定数据段寄存器的逻辑段地址,以方便通过标号访问数据。

  mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\  ; \是续行符,表示和下一行是一行内容,mytext标号用于后期引用
            'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
  number db 0,0,0,0,0

  start:
         mov ax,0x7c0                  ;设置数据段基地址 ,好处是段地址从0x0000改为0x07c0, mytext寻址从0x0000:7c00+mytext 改为 ds:mytext  即0x07c0:mytext,简化了后面程序的书写
         mov ds,ax

         mov ax,0xb800                 ;设置附加段基地址 
         mov es,ax

串传送指令movsb和movsw,了解8086的标志寄存器flags

标志寄存器flags,16位,有16个标志,从0-15, 6位ZF 零标志(处理器执行一条算数或逻辑运算指令后,如果计算结果为0,这一位置为1,如果结果非0,这一位置0),10位 DF 方向标志direction flag(清零或者置1,控制方向,通过指令改写).

         jmp near start ;程序被加载到7c00处,需要直接跳到代码处执行,而不是执行下面的mytext数据区,mytext如果放在程序尾部,可以不用jmp跳转

  mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\  ; \是续行符,表示和下一行是一行内容,mytext标号用于后期引用
            'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
  number db 0,0,0,0,0

  start:
         mov ax,0x7c0                  ;设置数据段基地址 ,好处是段地址从0x0000改为0x07c0, mytext寻址从0x0000:7c00+mytext 改为 ds:mytext  即0x07c0:mytext,简化了后面程序的书写
         mov ds,ax

         mov ax,0xb800                 ;设置附加段基地址 
         mov es,ax

         cld                            ;方向标志清零指令(flags寄存器的DF清零表示正方向传送),无操作数,std是置方向指令与cld相反,将flags寄存器的DF位置1,表示从高地址向低地址传送。
         mov si,mytext                ;设置 movsw的DS:SI寄存器
         mov di,0                     ;设置 movsw的ES:DI 目标位置的段地址:偏移地址
         mov cx,(number-mytext)/2      ;实际上等于 13,26个字节是13个字
         rep movsw                      ;movsw, 只能执行一次,需要反复执行需要结合rep前缀,重复的意思,重复次数由寄存器cx确定,cx不为零就继续执行,cx零时传送结束

$与$$ 记号,nasm编译器特有记号

  times 510-($-$$) db 0 ;$表示当前这一行的汇编地址,$$ 表示当前程序段的汇编地址
  db 0x55,0xaa

认识loop指令,循环指令,loop 标号,操作数是标号

8086处理器,loop执行过程:

将寄存器CX的内容减一;判断如果cx内容不为0,转移到指定的位置执行,否则顺序执行后面的指令。

8086处理器,只能使用BX,SI,DI,BP 提供偏移地址,不能使用其它寄存器。 寄存器BX在设计之初的作用之一就是用来提供数据访问的基地址,所以叫做基地址寄存器Base Address Register. 在设计8086处理器时,每个寄存器都有自己的特殊用途,比如AX是累加器Accumulator;CX是计数器Counter;DX是数据(data)寄存器,除了作为通用寄存器使用外,还专门用于和外设之间进行数据传送;SI是源索引寄存器Source Index; DI是目标索引寄存器Destination Index,用于数据传送操作,已经在movsb和movsw指令的使用过了。

inc指令: inc r/m ,inc用于将操作数加一 ,操作数可以使寄存器或者内存地址

DEC指令: dec r/m ,dec用于将操作数减一 ,操作数可以使寄存器或者内存地址

基址变址寻址和条件转移指令

在8086上只允许以下几种基址变址的组合

bx+si bx+di bp+si bp+di

         jmp near start ;程序被加载到7c00处,需要直接跳到代码处执行,而不是执行下面的mytext数据区,mytext如果放在程序尾部,可以不用jmp跳转

  mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\  ; \是续行符,表示和下一行是一行内容,mytext标号用于后期引用
            'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
  number db 0,0,0,0,0 ; 用于存储地址的字符,最大65535 

  start:
         mov ax,0x7c0                  ;设置数据段基地址 ,好处是段地址从0x0000改为0x07c0, mytext寻址从0x0000:7c00+mytext 改为 ds:mytext  即0x07c0:mytext,简化了后面程序的书写
         mov ds,ax

         mov ax,0xb800                 ;设置附加段基地址 
         mov es,ax

         cld                            ;方向标志清零指令(flags寄存器的DF清零表示正方向传送),无操作数,std是置方向指令与cld相反,将flags寄存器的DF位置1,表示从高地址向低地址传送。
         mov si,mytext                ;设置 movsw的DS:SI寄存器
         mov di,0                     ;设置 movsw的ES:DI 目标位置的段地址:偏移地址
         mov cx,(number-mytext)/2      ;实际上等于 13,26个字节是13个字
         rep movsw                      ;movsw, 只能执行一次,需要反复执行需要结合rep前缀,重复的意思,重复次数由寄存器cx确定,cx不为零就继续执行,cx零时传送结束

         ;得到标号所代表的偏移地址,将number的汇编地址传给寄存器ax,标号与数字等效,下面指令相当于把立即数传给寄存器
         mov ax,number

         ;计算各个数位,分解ax 的每个位的数字
         mov bx,ax
         mov cx,5                      ;循环次数 
         mov si,10                     ;除数 
  digit: 
         xor dx,dx                   ;dx异或置0 ,dx与ax形成32位的被除数,div用dx保存余数破坏被除数
         div si                     ;做除法操作
         mov [bx],dl                   ;保存数位,将余数保存到偏移地址[bx]处,[bx]就是number标号所表示的汇编地址,也是段内偏移地址,段地址是ds 0x7c0 ,[]表示保存到bx所表示的内存地址,省略[]表示寄存器bx内,8086处理器,只能使用BX,SI,DI,BP 提供偏移地址,不能使用其它寄存器。
         inc bx     ;inc用于将bx加一 ,操作数可以使寄存器或者内存地址
         loop digit        ;loop与标号digit之间的内容是循环体,操作码E2 8位相对偏移量,标号不能太远

         ;显示各个数位,
         mov bx,number 
         mov si,4         ;初始的索引值4传送到寄存器0,1,2,3,4 ,4是number的最后一个数位  ,从number的最后往前显示           
   show:
         mov al,[bx+si] ;这个加法运算不是在编译阶段运行,是在执行时是运行
         add al,0x30 ;获取asc码
         mov ah,0x04 ;颜色在高位
         mov [es:di],ax ;显示
         add di,2 ;指向显存下一个位置
         dec si  ; 指向number的下一个数位
         jns show ;如果标志寄存器的符号位不是1,则转移到标号show的位置处执行,8086寄存器flags的7位SF,算术运算结果的最高位是0,将sf置0,最高位是1,将sf置1,这里判断dec si运算后si的最高位,最后0-1后si借位后为11111111B,sf为1

         mov word [es:di],0x0744

         jmp near $ ;$表示当前这一行的汇编地址,跳转到自身

  times 510-($-$$) db 0  ;$表示当前这一行的汇编地址,$$ 表示当前程序段的汇编地址,相减表示前面内容的总字节数
  db 0x55,0xaa

计算机的负数

计算机中有符号数和无符号数

  jmp start
data1 db -1
data2 dw -25
start:
  mov ax,-78

无符号的8位数,8位全部用来表示数字,范围从0-255,共256个数

二进制 十进制 十六进制
00000000 0 0x00
00000001 1 0x01
00000010 2 0x02
... ... ...
11111111 255 0xFF

有符号的8位数,7位用来表示数字,最左面一位表示正负

二进制 十进制 十六进制
01111111 +127 0x7F
00000010 +2 0x02
00000000 0 0x00
11111111 -1 0xFF
11111110 -2 0xFE
10000000 -128 0x80

有符号的16位数 二进制| 十进制| 十六进制 --- | --- | --- 0111111111111111 | + 32767 | 0x7FFF 1111111111111111 | -1 | 0xFFFF 1000000000000000 | -32768 | 0x8000

11111101转十进制 -(12^7)+12^6+12^5+12^4+12^3+12^2+02^1+12^0=-3

得到一个整数的负值,学习SUB和NEG指令

mov dx,[0x3030]; 如何得到一个负数

获取负数通过0-正数获得,获得1的负数为0-1 二进制0-1结果是11111111B,二进制0-2 为11111110

SUB减法指令: sub r/m r/m/imm ,2个操作数宽度一致,不能同时为内存地址,结果保存在左操作数中。 neg求负数指令(对2的补码指令): neg r/m, 作用用0减去操作数,用结果替换操作数内容。操作正数得负数,操作负数的正数

  jmp start
data1 db -1
data2 dw -25
start:
  mov ax,-78
  mov ax,0
  sub ax,dx
  ;neg dx ;上面的3条指令可以用neg一条指令代替,neg求负数指令

计算机如何区分负数正数

mov ax,-1 mov bx,65535 ax和bx都是11111111

mov ah ,0xf0 inc ah

0xf0 无符号240,有符号-16 加一后,二进制11110001,无符号241,有符号-15

idiv有符号数除法指令 : idiv r/m 如果在指令中指定的是8位寄存器或者8位操作数的内存地址,则意味着被除数在寄存器AX里。 相除后,商在寄存器AL里,余数在寄存器AH里 idiv bh idiv byte [0x3030]

如果指令中指定的是16位寄存器或者16位操作数的内存地址,则意味着被除数是32位的,低16位在寄存器AX里;高16位在寄存器DX里。 相除后,商在寄存器AX里,余数在寄存器DX里 idiv bx idiv word [0x3030]

80386开始支持的,8086不支持 如果在指令中指定的是32位寄存器或者32位操作数的内存地址,则意味着被除数是64位的,低32位在寄存器eax里,高32位在寄存器edx里。 相除后,商在寄存器EAX里,余数在寄存器EDX里 idiv ebx idiv dword [0x3030]

64位cpu,8086和32位不支持 如果在指令中指定的是64位寄存器或者64位操作数的内存地址,则意味着被除数是128位的,低64位在寄存器rax里,高64位在寄存器rdx里。 相除后,商在寄存器RAX里,余数在寄存器RDX里 idiv rbx idiv qword [0x3030]

;以下为有符号数除法
mov ax,0x0400         ;0x0400等于正的1024
mov bl,0xf0           ; 0xf0 等于十进制-16
; mov ax,1024         ;与上面等效
; mov bl,-16
idiv bl               ;AL=0xC0
times 510-($-$$) db 0
db 0x55,0xaa

有符号数的符号扩展指令

8位有符号数范围 -128 - +127 16位有符号数范围 -32768 - +32767 32位有符号数范围 -2147483648 - +2147483647

+7 0000 0111 0x07 0000 0000 0000 0111 0x0007 0000 0000 0000 0000 0000 0000 0000 0111 0x00000007

-3 ,将8位扩展到16位相当于符号位扩展 1111 1101 0xFD 1111 1111 1111 1101 0xFFFD 1111 1111 1111 1111 1111 1111 1111 1101 0xFFFFFFFD

扩展指令 作用
cbw 将AL中的有符号数扩展到AX,若AL=FD (-3),扩展后AX=FFFD (-3)
cwde 将AX 中有符号数扩展到EAX,若AX=FFFD (-3),扩展后EAX=FFFFFFFD (-3)
cdqe 将EAX 中有符号数扩展到RAX,若AX=FFFFFFFD (-3),扩展后EAX=FFFFFFFFFFFFFFFD (-3)
cwd 将AX中的有符号数扩展到DX:AX,若AX=FFFD (-3),扩展后AX=FFFD (-3),DX=FFFF
cdq 将EAX中的有符号数扩展到EDX:EAX,若EAX=FFFFFFFD (-3),扩展后EAX=FFFFFFFD (-3),EDX=FFFFFFFF
cd0 将RAX中的有符号数扩展到RDX:RAX,若RAX=FFFFFFFFFFFFFFFD (-3),扩展后RAX=FFFFFFFFFFFFFFFD (-3),RDX=FFFFFFFFFFFFFFFF
;以下为有符号数除法
mov ax,-6002
cwd  ;扩展ax
mov bx,-10
idiv bx ;商600在ax,余数-2在dx中
times 510-($-$$) db 0 
db 0x55,0xaa

8086的标志寄存器

;在bochs调试器用info eflags观察以下内容
mov al,11111000b
add al,00001000b
times 510-($-$$) db 0
db 0x55,0xaa

条件转移指令,数字比较指令CMP

转移指令不影响任何标志位,但是依赖标志位进行跳转,必须放在影响标志位的指令之后。

dec si ;可能影响SF位
jns show

为了方便,将两个数进行比较来确定是否跳转,符合人们的习惯,这里可以使用cmp指令,cmp r/m r/m/imm ,影响CF,OF,SF,ZF,AF和PF标志位 cmp在功能上与sub相同,区别cmp根据结果设定标志位,不保留结果,不影响操作数原来的内容。 cmp al,35 cmp dx,ax cmp dx,[0x3030] cmp byte [0x3030],37 cmp [0x3030],ax

当寄存器里是个负数时转移到negb执行,jl 当小与时转移 cmp dh,0 jl negb

cmp dh,0x80 ;dh暂时看成无符号数,0x80是1000 0000,大于0x80的是负数,最高位都是1,小于的是正数,最高位都是0 jae negb ; jae高于等于转移

jcxz (jump if CX is zero),当CX为0时转移,当执行这条指令时,处理器先测试CX是否为0

例如 jcxz show

从1加到100

         jmp near start

 message db '1+2+3+...+100=' ;等同于 db '1','+',....,'=', 这个数据用于屏幕上显示

 start:
         mov ax,0x7c0           ;设置数据段的段基地址 
         mov ds,ax

         mov ax,0xb800          ;设置附加段基址到显示缓冲区
         mov es,ax

         ;以下显示字符串 
         mov si,message      ;将字符串首地址(汇编地址)传给si    
         mov di,0            ;di 作为显存偏移地址
         mov cx,start-message ;以来cx设置loop循环次数
     @g:
         mov al,[si]          ; 从数据段ds偏移地址si代表的内存地址(ds:[si])取数,给al
         mov [es:di],al       ;将al内容写入显存
         inc di                ;递增di的值,指向下一个显存地址
         mov byte [es:di],0x07 ;字符颜色属性
         inc di                 ;递增di的值,指向下一个显存地址
         inc si                 ;递增四si,指向数据的下一个字符
         loop @g              ;loop循环显示,以来cx计数,cx不是0,继续从@g处循环

         ;以下计算1到100的和 
         xor ax,ax             ;寄存器ax用于存放累加结果,先用xor异或清零ax
         mov cx,1              ;cx用于充当加数
     @f:
         add ax,cx        ;第一次执行时是0+1,第二次是1+2,第三次是3+3
         inc cx            ;递增cx值
         cmp cx,100        ; 比较cx与100
         jle @f             ;如果cx 小于等于100 跳转到@f
         jmp  $
times 510-($-$$) db 0
     db 0x55,0xaa

栈的原理和栈的操作方法

push r/m ;推栈、压栈,从内存或者寄存器中把数据放入栈中 push dx push word [0x3030] pop r/m ; 弹栈、出栈,把栈中数据取出放入内存或者寄存器中 pop ax ;从栈取数据存入ax pop word [0x08] ;从栈取数据一个字长度存入内存地址0x08处

SP栈指令指针寄存器,用来访问栈的数据,栈顶寄存器 SS寄存器提供栈段的基础地质

;假设SS=256A,SP=0200
mov ax,25  
mov bx,30
mov cx,35

push ax ;将sp-2, sp=01fe,将寄存器sp的值叫做栈顶,物理地址为2589E,将ax的值写入2589E和2589F
push bx
push cx
pop dx ;先根据ss:SP寻址,取数据放入dx,sp再+2
pop si
pop di

压栈从高地址位向低地址位推进,出栈从低地址位向高地址位出栈;与cs\ds\es不同。push指令执行不影响任何标志位

         jmp near start

 message db '1+2+3+...+100=' ;等同于 db '1','+',....,'=', 这个数据用于屏幕上显示

 start:
         mov ax,0x7c0           ;设置数据段的段基地址 
         mov ds,ax

         mov ax,0xb800          ;设置附加段基址到显示缓冲区
         mov es,ax

         ;以下显示字符串 
         mov si,message      ;将字符串首地址(汇编地址)传给si    
         mov di,0            ;di 作为显存偏移地址
         mov cx,start-message ;以来cx设置loop循环次数
     @g:
         mov al,[si]          ; 从数据段ds偏移地址si代表的内存地址(ds:[si])取数,给al
         mov [es:di],al       ;将al内容写入显存
         inc di                ;递增di的值,指向下一个显存地址
         mov byte [es:di],0x07 ;字符颜色属性
         inc di                 ;递增di的值,指向下一个显存地址
         inc si                 ;递增四si,指向数据的下一个字符
         loop @g              ;loop循环显示,以来cx计数,cx不是0,继续从@g处循环

         ;以下计算1到100的和 
         xor ax,ax             ;寄存器ax用于存放累加结果,先用xor异或清零ax
         mov cx,1              ;cx用于充当加数
     @f:
         add ax,cx        ;第一次执行时是0+1,第二次是1+2,第三次是3+3
         inc cx            ;递增cx值
         cmp cx,100        ; 比较cx与100
         jle @f             ;如果cx 小于等于100 跳转到@f

         ;以下计算累加和的每个数位 
         xor cx,cx              ;设置堆栈段的段基地址
         mov ss,cx              ;将cx的值0传给ss
         mov sp,cx              ;将0传送给栈指针寄存器

         mov bx,10              ;除数10传给寄存器bx,用来分解ax
         xor cx,cx              ;将cx清零,这里cx用来记录实际分解了多少个数位(ax如果比较小)
     @d:
         inc cx                 ;用cx+1,来确定分解ax的次数
         xor dx,dx              ;将dx清零,dx与ax形成32位的被除数
         div bx                 ;除法,余数再dx中,商再ax中
         add dl,0x30                ;dl+0x30得到余数的字符编码值
         push dx                ;在16位处理上压栈出栈数据长度必须是16位(字),不能是字节,第一次压栈sp-2=0xFFFE, 0000:FFFE是第一个数据的地址
         cmp ax,0               ;对比商是否还能分解
         jne @d                 ; ax不等于0 则跳转

         ;以下显示各个数位 ,loop用到了cx,cx在前面被设置为ax除10分解的次数
     @a:
         pop dx                 ;弹出栈的数据放入dx,dx的低8位是字符的字符编码
         mov [es:di],dl
         inc di
         mov byte [es:di],0x07
         inc di
         loop @a

         jmp near $ 


times 510-($-$$) db 0
         db 0x55,0xaa

push ax 等价于 sub sp,2 mov bx,sp mov [ss:bx],ax pop ax等价于 mov bx,sp mov ax,[ss:bx] add sp,2

必须保持栈平衡,压栈和出栈数据要相同,要充分估计需要的栈空间,否则会破坏其他数据。

逻辑指令or 逻辑与指令and

0 or 0 = 0 0 or 1 = 1 1 or 0 = 1 1 or 1 = 1

129 or 127 = 255 1000 0001 or 0111 1111 = 1111 1111

or r/m r/m/imm 或运算结果保存左操作数中,操作数长度必须一致;or执行后OF=0,CF=0 ,SF\ZF\PF依计算结果而定,AF状态未定义。

and r/m r/m/imm 与运算结果保存在左操作数中,操作数长度必须一致(与门有2个输入1个输出)

0 and 0 = 0 0 and 1 = 0 1 and 0 = 0 1 and 1 = 1

129 and 127 = 1 1000 0001 and 0111 1111 = 0000 0001

8086寻址方式

addressing mode,寻址方式是如何找到要操作的数据和如何找到结果要存放的地方

buffer dw 0x20,0x100,0x0f,0x300,0xff00
;以下使用直接寻址方式将所有数据加1
inc word [buffer]
inc word [buffer+2]
inc word [buffer + 4]
inc word [buffer + 6]
inc word [buffer + 8]
;以下使用基址寻址方式加一
  mov bx,buffer
  mov cx,5
lpinc:
  inc word [bx]
  add bx,2
  loop lpinc ;判断cx是否为0,非0跳转

;以下方式,查看栈里面的数据,用bx作为偏移地址默认的基址寄存器是ds,如果ds:bx时,ds段超越前缀可以省略,bp默认段基址寄存器是ss,ss:bp时可以省略ss段超越前缀
mov bx,sp
mov dx,[ss:bx+2]
;下面语句与上面相同
;mov bp,sp
;mov dx,[bp+2]

bx默认访问数据段,bp默认访问栈段。

;使用变址寻址,没有提供段超越前缀,默认使用ds提供段地址
mov [si],dx ;将dx的内容传送到[ds:si]处
add ax,[di]
xor word [si],0x8000
;变址寻址方式+偏移量
mov [si+0x100],al
and byte [di+mydata],0x80

反转字符串

jmp start
string db 'abcdefghijklmnopqistuvwxyz'
start:
  mov ax,0x7c0              ;设置数据段基地址为7c00,访问偏移地址比较方便,标号地址即为偏移地址
  mov ds,ax

  mov ax,cs                 ;设置栈段地址,ss与cs相同,cs不允许直接设置ss,通过ax中转
  mov ss,ax
  mov sp,0                  ;初始化栈顶指针

  mov cx,start-string       ;循环次数从26到1,共26次
  mov bx,string             ;数据区首地址

lppush:
  mov al,[bx]
  push ax
  inc bx
  loop lppush               ;循环压栈

  mov cx,start-string
  mov bx,string

lppop:
  pop ax
  mov [bx],al
  inc bx
  loop lppop                ;循环出栈

  jmp $

  times 510-($-$$) db 0
  db 0x55,0xaa

[bx + si] [bx+di] [bx + si + 偏移量] [bx + di + 偏移量] mov ax, [bx+si+0x30] [bp + si] [bp+di] [bp + si + 偏移量] [bp + di + 偏移量] mov ax, [bp+si+0x30] ;bp默认段寄存器ss

反转字符串

jmp start
string db 'abcdefghijklmnopqistuvwxyz'
start:
  mov ax,0x7c0              ;设置数据段基地址为7c00,访问偏移地址比较方便,标号地址即为偏移地址
  mov ds,ax

  mov bx,string             ;数据区首地址
  mov si,0                  ;正向索引号,当si小于di时进行数据交换
  mov di,start-string-1         ;反向索引号

rever:
  mov ah,[bx+si]
  mov al,[bx+di]
  mov [bx+si],al
  mov [bx+di],ah  ;上面四行用于交换收尾数据

  inc si
  dec di
  cmp si,di
  jl rever  ;si没有相遇或者超越继续交换

  jmp $

  times 510-($-$$) db 0
  db 0x55,0xaa

硬盘、显卡访问与控制

编写多个段的汇编程序

段寄存器cs用于保存代码段基地址,DS,ES用于保存数据段地址,段寄存器SS,用于保存栈段地址。一个规范的汇编程序应当包含代码段,附加段,数据段,栈段,这样这个段的界限在程序加载前就划分好了,如果1个数据段长度超过64kB,需要拆分为多个数据段,其他段同样。

NASM用section或者segment定义段,段定义以section或者segment开始 + 段名称;如果程序中没有段定义语句,这个程序自成一个段,段的长度必须为4字节的倍数,不足使用0填充,段可以使用ALIGN=16强行使段按16字节对齐(8086按16字节对齐)。

ESCTION data1 ALIGN=16
mydata dw 0xFACE

SECTION data2 ALIGN=16
string db 'hello'

section code ALIGN=16
  mov bx,mydata
  mov si,string

控制段内元素的汇编地址

VSTART=0字句,虚拟的汇编地址,使用这个字句后这个段内所有的汇编地址都从这个段开始计算。

ESCTION data1 ALIGN=16 VSTART=0
mydata dw 0xFACE

SECTION data2 ALIGN=16 VSTART=0
string db 'hello'

section code ALIGN=16 VSTART=0
  mov bx,mydata
  mov si,string
section s1
offset dw str1,str2,num
section s2 align=16 vstart=0x100
str1 db 'hello'
str2 db 'world'
section s3 align=16
num dw 0xbad

求offset 处3个字的具体数值(即求str1\str2\num的汇编地址)

加载器、用户程序(认识用户程序的头部构造)

用户程序头部协议:程序的总长度(字节大小,决定读取多少逻辑扇区),入口点,段重定位表项数,段重定位表。

section.段名字.start 这个表达式计算一个段相对于程序开头的汇编地址,即相对程序开头的偏移量,即这个段距离程序开头的字节数 段重定位表以标号 segtbl_begin:开始以标号 segtbl_end:结束

;包含代码段、数据段、栈段的用户程序
;========================
section header vstart=0                ;用户程序头部段
  program_length dd program_end        ;程序总长度[0x00]
  ;用户程序入口
  code_entry dw start                   ;start表示入口点在code段的段内偏移地址[0x04]
             dd section.code.start      ;段偏移地址,即入口点的汇编地址,与程序起止处的偏移量[0x06]
  realloc_tbl_len dw (segtbl_end-segtbl_begin)/4  ;段重定位表项个数[0x0A],用户程序段数目不确定,必须计算出来

  ;段重定位表,标号之间是表项,每个项目用于记载段的汇编地址,用双字dd定义,如果程序很大,段的汇编地址也会很大,用32位表示段的汇编地址
  segtbl_begin:
  code_segment dd section.code.start ;[0x0c]
  data_segment dd section.data.start ;[0x10]
  stack_segment dd section.stack.start ;[0x14]
  segtbl_end:

;=================================
section code align=16 vstart=0 ;代码段
  start:
    ;初始执行时,DS和ES指向用户程序头部
    mov ax,[stack_segment] ;设置到用户程序自己的堆栈
    mov ss,ax
    mov sp,stack_pointer  ;设置初始的栈顶指针

    mov ax,[data_segment] ;设置用户程序自己的数据段
    mov ds,ax

    mov ax,0xb800
    mov es,ax

    mov si,message
    mov di,0

  next:
    mov al,[si]
    cmp al,0
    je exit
    mov byte [es:di],al
    mov byte [es:di+1],0x07
    inc si
    add di,2
    jmp next
  exit:
    jmp $

;===========================================
section data align=16 vstart=0 ;数据段
  message db 'hello world.',0    

;=====================================
section stack align=16 vstart=0  ;栈段
  resb 256  ;编译时这里有个警告可以忽略,表示未初始化这个栈的数据
  stack_pointer:

;===========================================
section trail align=16 ;尾部
program_end:

上面程序用图表示:

确定用户程序加载的内存地址:

外围设备一般分2中,输入设备和输出设备,硬盘即可以输入也可以输出。Input/Output 简称I/O。

i/o 接口是信号转换器和总线收发控制器,由io就扣负责和处理器交流,端口是io接口内部的寄存器,端口是处理器和外围设备交流的窗口,每io接口可能有多个端口,每个端口有不同的作用,如命令端口,数据端口,状态端口等,每个端口有一个端口号,是个数字

intel处理器只允许65536个端口,端口号从0-65535,端口访问与内存访问不同,不能使用mov指令要用in、out指令

in al, dx ;目的操作数是al ax,原操作数是dx(dx存放端口号),访问8位端口使用al,访问16位端口使用ax,in指令不影响标志位 in ax, dx in al,imm8 ;如果端口号小于256可以,原操作数可以用立即数 in ax,imm8

example:

mov dx,0x3c0 ;0x3c0是端口号
in ax,dx
in al,0x60 ;从端口0x60读入一个字节

out dx/imm8 ,al,ax ;目标端口号可以是dx或者imm小于256的立即数,源操作数在al或者ax中

硬盘读写的基本单位是扇区,要读至少读一个扇区,要写至少写一个扇区,不会读写一个扇区的几个字节,

块Block 设备 CHS模式 (Cylinder.Header.Sector) ,最原始的模式,读写磁盘要向扇区发送磁头号Header,柱面号Cylinder、扇区Sector在柱面的编号, LBA模式(Logical Block Addressing),逻辑块寻址,LBA28,使用28个比特表示逻辑扇区号。

个人计算器主硬盘控制器有8个端口 ,端口号从0x1f0-0x1f7,从硬盘读取逻辑扇区的过程如下:

1.设置要读取的扇区数量,要写入0x1f2端口

mov dx,0x1f2   ;将端口号保存到dx寄存器
mov al,0x01   ;要读写的扇区数量1,写入al寄存器
out dx,al     ;向dx端口发送要读取的扇区数量,将1写入0x1f2端口,al 的1-255表示读写1-255个扇区,如果al是0表示要读写256个扇区,有点特殊

2.设置起始的扇区号

  1. 请求读
mov dx ,0x1f7 ;向0x1f7端口发送读
mov al,0x20 ;读命令
out dx,al

4.发送请求后判断硬盘是否已经准备好读写了,0x1f7的位7是0,位3是1,表示可以读数据,将al与操作后位00001000,表示可以操作,jnz 如果上面比较不为0(al不是0x80)就跳转。

5.连续取数据

;假定DS已经只想存放扇区数据的段,bx里是段内偏移地址
  mov cx,256  ;要读取的总字数,256*2为512字节,256是loop的次数
  mov dx,0x1f0
.readw:
  in ax,dx
  mov [bx],ax
  add bx,2
  loop .readw

过程和过程调用

过程调用与过程返回的原理 过程调用 call read_hard_disk_0 ,call + 标号 ,标号是子程序的名字、入口, 对应的机器码操作码:0xE8 +16位相对偏移量(一共3个字节,偏移量用标号处的汇编地址减去call指令的下一条指令的汇编地址,保留16位结果), 这种在8086上的调用叫做16位相对近调用。被调用的过程位于当前代码段内,不是其他代码段,只需要知道目标位置的偏移地址即可

从硬盘加载整个用户程序

用户程序重定位

入口点的重定位

比特位移动指令

shr ax,4 ;将ax的内容整体右移4位,shr r/m,imm8/cl ,标志寄存器的CF位等于最后进来的一个比特m

ror dx,4 ;ror循环右移指令将dx内容右移4位 ror r/m,imm8/cl

shl : 逻辑左移 rol : 循环左移

8086 无条件转移指令

相对短转移和16位相对近转移 jmp short 标号/目标处汇编地址,操作码EB+1字节偏移量 jmp near 标号/目标处汇编地址,操作码E9+2个字偏移量 jmp 标号/目标处汇编地址,编译器决定是short还是near

16位间接绝对近转移

jmp r/m,用操作数的值直接替换指令指针寄存器ip的内容

example: jmp cx jmp [0x3030] jmp [bx]

16位直接绝对远转移指令 ,用段地址代替cs内容,用偏移地址代替ip内容 jmp 16位段地址:16位偏移地址

jmp 0x052e:0x003c

16位间接绝对远转移

jmp far m ;m处包含2个字,第一个字包含偏移地址,替换ip内容,第二个字包含目标地址,替换cs内容 示例: jmp far [0x2002] jmp far [bx]

resb 256 ;表示未初始化这个栈的数据,reserved bytes 保留若干字节 resw 128 ; 字为单位 resd 64 ; 以双字为单位进行保留

mbr.asm程序和app.asm程序代码:

         ;文件名:c08_mbr.asm
         ;文件说明:硬盘主引导扇区代码(加载程序)
         ;加载器需要查找内存中空闲的位置,把程序加载到内存的什么位置,用户程序位于硬盘的什么位置
         app_lba_start equ 100           ;声明常数(规定用户程序起始逻辑扇区号为100)
                                         ;常数的声明不会占用汇编地址

SECTION mbr align=16 vstart=0x7c00             ;将引导扇区定义一个段mbr ,使用vstart=0x7c00将段内所有元素汇编地址从0x7c00开始计算                    

         ;设置堆栈段和栈指针 
         mov ax,0      
         mov ss,ax
         mov sp,ax

         mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
         mov dx,[cs:phy_base+0x02]
         mov bx,16        
         div bx                         ; 0x10000除16得到16位的段地址0x1000在ax中
         mov ds,ax                       ;令DS和ES指向该段以进行操作
         mov es,ax                        ;这之前的代码用来确定用户程序的段地址



         ;以下读取程序的起始部分,用户程序的第一个扇区 
         xor di,di
         mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
         xor bx,bx                       ;加载到DS:0x0000处 
         call read_hard_disk_0           ;读硬盘,将指令指针当前的内容(指向mov dx,[2]的ip值)压栈,ret时再出栈,call近调用代码段cs寄存器值不变,不需要将cs压栈

         ;以下判断整个程序有多大
         mov dx,[2]                      ;取出用户程序总长度的高16位mov ds:dx,ds:[2]
         mov ax,[0]                      ;取出用户程序总长度的低16位
         mov bx,512                      ;512字节每扇区
         div bx                          ; 用户程序总长度/512=用户程序使用的扇区数   ,有可能有余数表示最后一个扇区没满512字节
         cmp dx,0                        ;dx保存的是余数,不能与0,没有除尽,总扇区数应为ax+1,因为已经读取了一个扇区,剩下的要读取的扇区为ax+1-1=ax
         jnz @1                          ;未除尽,因此结果比实际扇区数少1 
         dec ax                          ;如果除尽了ax正好是用户扇区总数,因为已经读取了1个,扇区总数减1 
   @1:
         cmp ax,0                        ;考虑实际长度小于等于512个字节的情况 ,小于等于512不用再读扇区了
         jz direct

         ;读取剩余的扇区
         push ds                         ;以下要用到并改变DS寄存器 

         mov cx,ax                       ;循环次数(剩余扇区数)
   @2:
         mov ax,ds
         add ax,0x20                     ;得到下一个以512字节为边界的段地址
         mov ds,ax                      ;构造下一个逻辑段地址,防止读取数据超过DS:0xFFFF越界

         xor bx,bx                       ;每次读时,偏移地址始终为0x0000 
         inc si                          ;下一个逻辑扇区 
         call read_hard_disk_0
         loop @2                         ;循环读,直到读完整个功能程序 

         pop ds                          ;恢复数据段基址到用户程序头部段 

         ;计算入口点代码段基址 
   direct:
         mov dx,[0x08] ;从入口点所在代码段的汇编地址的高16位存到dx中
         mov ax,[0x06] ;从入口点所在代码段的汇编地址的低16位存到ax中,ds目前指向用户程序头部地址
         call calc_segment_base ; 计算用户程序起始处的逻辑段地址
         mov [0x06],ax                   ;回填修正后的入口点代码段基址 

         ;开始处理段重定位表
         mov cx,[0x0a]                   ;需要重定位的项目数量
         mov bx,0x0c                     ;重定位表首地址

 realloc:
         mov dx,[bx+0x02]                ;32位地址的高16位 
         mov ax,[bx]
         call calc_segment_base
         mov [bx],ax                     ;回填段的基址
         add bx,4                        ;下一个重定位项(每项占4个字节) 
         loop realloc 

         jmp far [0x04]                  ;间接绝对远转移指令跳到用户程序,从0x04位置依次取出ip和cs  

;-------------------------------------------------------------------------------
read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                         ;输入:DI:SI=起始逻辑扇区号
                                         ;      DS:BX=目标缓冲区地址
         push ax
         push bx
         push cx
         push dx                ;压栈,保存寄存器,用于恢复寄存器

         mov dx,0x1f2   ;设置端口
         mov al,1   ;读取的扇区数
         out dx,al                       ;读取的扇区数发送给端口
        ;91-101 用于向硬盘接口 写入起始逻辑扇区号的低24位
         inc dx                          ;端口号0x1f3
         mov ax,si                      ;si给ax,后面把ax分成高低2个8位
         out dx,al                       ;LBA地址位7~位0

         inc dx                          ;端口号0x1f4
         mov al,ah
         out dx,al                       ;LBA地址位15~位8

         inc dx                          ;端口号0x1f5
         mov ax,di
         out dx,al                       ;LBA地址位23~位16

         inc dx                          ;0x1f6
         mov al,0xe0                     ;LBA28模式,主盘,0xe是控制参数
         or al,ah                        ;LBA地址27~24,得到控制组合值
         out dx,al

         inc dx                          ;0x1f7
         mov al,0x20                     ;读命令
         out dx,al

  .waits:
         in al,dx
         and al,0x88
         cmp al,0x08
         jnz .waits                      ;不忙,且硬盘已准备好数据传输 

         mov cx,256                      ;总共要读取的字数
         mov dx,0x1f0
  .readw:
         in ax,dx
         mov [bx],ax
         add bx,2
         loop .readw

         pop dx
         pop cx
         pop bx
         pop ax

         ret ;return指令,从栈中将ip弹出,ip改变为调用子程序之前的值

;-------------------------------------------------------------------------------
calc_segment_base:                       ;计算16位段地址
                                         ;输入:DX:AX=32位,用于生成32位物理地址
                                         ;返回:AX=16位段基地址 
         push dx                          

         add ax,[cs:phy_base] ; 将地址的低16位与ax相加,可能产生进位(标志寄存器cf会变1)
         adc dx,[cs:phy_base+0x02]  ;adc待进位加法指令,加上cf标志位0或1
         shr ax,4
         ror dx,4
         and dx,0xf000 ; 确保dx的低12位是0,这一步可以省略
         or ax,dx ; ax是最终逻辑段地址

         pop dx

         ret

;-------------------------------------------------------------------------------
         phy_base dd 0x10000             ;用户程序被加载的物理起始地址,用一个双字phy_base存放,这个0x10000可以改为其他地址,只要这个内存空间空闲可用,唯一要求是这个地址最后一位是0,必须是16字节对齐

 times 510-($-$$) db 0
                  db 0x55,0xaa
;包含代码段、数据段、栈段的用户程序app.asm
;========================
section header vstart=0                ;用户程序头部段
  program_length dd program_end        ;程序总长度[0x00]
  ;用户程序入口
  code_entry dw start                   ;start表示入口点在code段的段内偏移地址[0x04]
             dd section.code.start      ;段偏移地址,即入口点的汇编地址,与程序起止处的偏移量[0x06]
  realloc_tbl_len dw (segtbl_end-segtbl_begin)/4  ;段重定位表项个数[0x0A],用户程序段数目不确定,必须计算出来

  ;段重定位表,标号之间是表项,每个项目用于记载段的汇编地址,用双字dd定义,如果程序很大,段的汇编地址也会很大,用32位表示段的汇编地址
  segtbl_begin:
  code_segment dd section.code.start ;[0x0c]
  data_segment dd section.data.start ;[0x10]
  stack_segment dd section.stack.start ;[0x14]
  segtbl_end:

;=================================
section code align=16 vstart=0 ;代码段
  start:
    ;初始执行时,DS和ES指向用户程序头部
    mov ax,[stack_segment] ;设置到用户程序自己的堆栈
    mov ss,ax
    mov sp,stack_pointer  ;设置初始的栈顶指针

    mov ax,[data_segment] ;设置用户程序自己的数据段,ds要最后设置
    mov ds,ax

    mov ax,0xb800   ;设置显存
    mov es,ax

    mov si,message ;ds:si字符串起始地址
    mov di,0  ;es:di显存起始地址

  next:
    mov al,[si]
    cmp al,0  ;查看字符串是否结束
    je exit
    mov byte [es:di],al
    mov byte [es:di+1],0x07
    inc si
    add di,2
    jmp next
  exit:
    jmp $

;===========================================
section data align=16 vstart=0 ;数据段
  message db 'hello world.',0    ;这个0作为取字符串取到0时表示字符串结束

;=====================================
section stack align=16 vstart=0  ;栈段
  resb 256  ;编译时这里有个警告可以忽略,表示未初始化这个栈的数据,reserved bytes 保留若干字节
  ;可以用times 256 db 0 代替
  stack_pointer:    ; stack_pointer的汇编地址是这256字节之后的汇编地址,作为栈顶指针,这里sp是256

;===========================================
section trail align=16 ;尾部
program_end:

控制与文件显示有关的回车、换行、光标

回车0x0d 换行0x0a

光标在屏幕上的位置保存在显卡的2个光标寄存器中,每个8位,合起来16位,可读可写,光标的大致控制过程如下:

从80386开始8086不支持

如果在指令中指定的是32位寄存器或者32位操作数的内存地址,则意味着被乘数是32位的,在寄存器EAX里 相乘后,乘积是64位的,低32位在寄存器EAX里,高32位在寄存器EDX里 mul ebx
mul dword [0x3030]

64位

如果在指令中指定的是64位寄存器或者64位操作数的内存地址,则意味着被乘数是64位的,在寄存器RAX里 相乘后,乘积是128位的,低64位在寄存器RAX里,高64位在寄存器RDX里 mul rbx
mul qword [0x3030] ;含

imul有符号的正数乘法

;===============================================================================
SECTION header vstart=0                     ;定义用户程序头部段 
    program_length  dd program_end          ;程序总长度[0x00],初始化dd双字,program_end一个32位的数据,program_end定义在程序尾部,在编译阶段将program_end的汇编地址填写到这里

    ;用户程序入口点
    code_entry      dw start                ;偏移地址[0x04],用start代表的汇编地址代替。
                    dd section.code_1.start ;段地址[0x06] 

    realloc_tbl_len dw (header_end-code_1_segment)/4
                                            ;段重定位表项个数[0x0a]

    ;段重定位表           
    code_1_segment  dd section.code_1.start ;[0x0c]
    code_2_segment  dd section.code_2.start ;[0x10]
    data_1_segment  dd section.data_1.start ;[0x14]
    data_2_segment  dd section.data_2.start ;[0x18]
    stack_segment   dd section.stack.start  ;[0x1c]

    header_end:                

;===============================================================================
SECTION code_1 align=16 vstart=0         ;定义代码段1(16字节对齐) 
put_string:                              ;显示串(0结尾);输入:DS:BX=串地址
         mov cl,[bx]
         or cl,cl                        ;cl=0 ?
         jz .exit                        ;是的,返回主程序 
         call put_char
         inc bx                          ;下一个字符 
         jmp put_string

   .exit:
         ret

;-------------------------------------------------------------------------------
put_char:                                ;显示一个字符
                                         ;输入:cl=字符ascii
         push ax
         push bx
         push cx
         push dx
         push ds
         push es

         ;以下取当前光标位置
         mov dx,0x3d4 ;操作显卡索引端口
         mov al,0x0e ;要操作0x0e寄存器
         out dx,al
         mov dx,0x3d5 ; 操作数据端口0x3d5,
         in al,dx                        ;高8位, 从0x0e读出1个字节数据
         mov ah,al  ;将光标位置高8位传给ah

         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;将光标位置低8位 给al
         mov bx,ax                       ;BX=代表光标位置的16位数,ax马上要使用,bx临时保存一下

         cmp cl,0x0d                     ;回车符?回车不执行jnz,执行mov ax,bx
         jnz .put_0a                     ;不是。看看是不是换行等字符 
         mov ax,bx                       ;此句略显多余,但去掉后还得改书,麻烦 
         mov bl,80                       
         div bl
         mul bl
         mov bx,ax
         jmp .set_cursor

 .put_0a:
         cmp cl,0x0a                     ;换行符?
         jnz .put_other                  ;不是,那就正常显示字符 
         add bx,80
         jmp .roll_screen

 .put_other:                             ;正常显示字符
         mov ax,0xb800
         mov es,ax
         shl bx,1
         mov [es:bx],cl

         ;以下将光标位置推进一个字符
         shr bx,1
         add bx,1

 .roll_screen:
         cmp bx,2000                     ;光标超出屏幕?滚屏
         jl .set_cursor

         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
         mov bx,3840                     ;清除屏幕最底一行
         mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

         mov bx,1920

 .set_cursor:
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         mov al,bh
         out dx,al
         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         mov al,bl
         out dx,al

         pop es
         pop ds
         pop dx
         pop cx
         pop bx
         pop ax

         ret

;-------------------------------------------------------------------------------
  start:
         ;初始执行时,DS和ES指向用户程序头部段
         mov ax,[stack_segment]           ;设置到用户程序自己的堆栈 
         mov ss,ax
         mov sp,stack_end

         mov ax,[data_1_segment]          ;设置到用户程序自己的数据段
         mov ds,ax

         mov bx,msg0
         call put_string                  ;显示第一段信息 

         push word [es:code_2_segment]
         mov ax,begin
         push ax                          ;可以直接push begin,80386+

         retf                             ;转移到代码段2执行 

  continue:
         mov ax,[es:data_2_segment]       ;段寄存器DS切换到数据段2 
         mov ds,ax

         mov bx,msg1
         call put_string                  ;显示第二段信息 

         jmp $ 

;===============================================================================
SECTION code_2 align=16 vstart=0          ;定义代码段2(16字节对齐)

  begin:
         push word [es:code_1_segment]
         mov ax,continue
         push ax                          ;可以直接push continue,80386+

         retf                             ;转移到代码段1接着执行 

;===============================================================================
SECTION data_1 align=16 vstart=0

    msg0 db '  This is NASM - the famous Netwide Assembler. '
         db 'Back at SourceForge and in intensive development! '
         db 'Get the current versions from http://www.nasm.us/.'
         db 0x0d,0x0a,0x0d,0x0a
         db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
         db '     xor dx,dx',0x0d,0x0a
         db '     xor ax,ax',0x0d,0x0a
         db '     xor cx,cx',0x0d,0x0a
         db '  @@:',0x0d,0x0a
         db '     inc cx',0x0d,0x0a
         db '     add ax,cx',0x0d,0x0a
         db '     adc dx,0',0x0d,0x0a
         db '     inc cx',0x0d,0x0a
         db '     cmp cx,1000',0x0d,0x0a
         db '     jle @@',0x0d,0x0a
         db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
         db 0

;===============================================================================
SECTION data_2 align=16 vstart=0

    msg1 db '  The above contents is written by LeeChung. '
         db '2011-05-06'
         db 0

;===============================================================================
SECTION stack align=16 vstart=0

         resb 256

stack_end:  

;===============================================================================
SECTION trail align=16  ;改段没有vstart,汇编地址从程序开头计算
program_end:

16位相对近调用,call 标号或者目标处的汇编地址,编译后3个字节,操作码E8+2字节偏移量,属于段内调用,ip压栈保存。示例:call dest

16位间接绝对近调用,只能调用当前代码段内,call r/m , 示例 call cx; call [0x3030];call [bx] ;要将ip压栈保存。

16位直接绝对远调用,跨段调用,call 16位段地址:16位偏移地址 ,示例call 0x0520:0x003c, cs和ip都改变,原cs,ip都要压栈

16位间接绝对远调用,call far m ,示例 call far [0x3030], call far [bx] ,cs和ip都从内存获取,原cs和ip都要压栈,

远过程返回指令retf转到另一个代码段内执行。

近过程调用和返回方式,用call调用,压入ip寄存器值,在过程内部使用ret返回。 远调用过程的返回方式,用call far 调用过程,压入cs和ip,在过程内部使用retf返回。

中断动态时钟显示

8259A芯片:Intel处理器允许256个中断,8259A提供其中15个,改芯片有自己的端口号,可以像访问其他设备一样用in/out 访问控制(设置中断信号优先级、屏蔽某些中断信号),又叫做可编程控制器,每个芯片有8个输入,个人计算机有2块8259A芯片,主片IRO接系统定时器引脚,从片IR0接定时时钟RTC引脚。

在8259A内部有中断屏蔽寄存器IMR ,有8位,每个bit位对应IR0-IR7 8个引脚输入,对应bit为0表示允许这个中断信号进入,对应bit为1表示阻止这个中断信号进入。

中断是否被处理还与标志寄存器IF位有关,当IF 为0时,所有从INTR来的中断信号都被忽略掉,当IF为1时, 处理器可以接受响应中断。cli 设IF 0,sti设IF为1。

中断优先级:从IR0到IR7依次降低。

中断号(如上图所示):为了区分不同的中断,每个中断都有一个编号,叫做中断号,8259A芯片每个引脚的中断号是可以改变的,刚开机时每个引脚的中断号由bios设置,cpu收到中断信号要处理时,会让芯片从总线将中断号发送过来。

中断处理过程:对中断信号的处理是通过执行一段代码完成的,设备发出中断的原因是设备状态的改变或有数据传送,因此可以事先编写代码解决这些问题,这段代码叫做中断处理程序或中断处理过程,在实模式下通过中断号访问中断向量表,从中取出中断处理程序的逻辑段地址和偏移地址,跳转并执行。

中断向量表: Interrupt VectorTable :IVT ,每个中断占2个字,由bios开机时创建,知道中断号*4就得到中断程序的段地址和偏移地址

中断发生时处理器的处理过程: 1 保护中断现场。将标志寄存器压栈,代码段cs指令指针寄存器ip压栈。 2 执行中断处理程序。 3 在遇到iret(中断返回指令 Interrupt Return :IRET,无操作数)指令时,返回到断点接着执行。恢复寄存器中断前状态。

实时时钟 ,CMOS RAM , 日期和时间的存储,BCD编码

实时时钟RTC(Real Time Clock) :用来提供日期和时间 ,关闭计算机依然运行,为所有需要时间的软硬件提供服务,RTC负责计时,日期和时间数值存储在CMOS(互补金属氧化物半导体)RAM芯片(通常128个字节)中,(典型RTC芯片摩托罗拉MC146818A),目前被集成在输入输出控制器ICH(南桥芯片)内。RTC由石英晶体振荡器驱动(晶振器),经过分频后每秒1次都cmos时间进行刷新,处理保存时间RTC可以提供闹钟和周期性的中断功能。

CMOS容量在64-256字节容量之间,日期和时间只占用以小部分容量,剩余容量保存整机信息,开机密码,硬件信息,启动顺序等等。=常规日期和时间占据cmos前10个字节。后面四个寄存器用于设置RTC工作状态,cmos访问需要4个端口进行2个索引端口,2个数据端口。索引端口通常只用0x70即可,数据端口通常只用0x71即可。

CMOS中的日期和时间通常以二进制形式的十进制编码保存(BCD Binary Coded Decimal),这是默认方式如果需要可以设置正常二进制。

25 --> 二进制 0001 1001 --> BCD 编码(将一个字节低四位和高四位分别表示0-9) 0010 0101

RTC电路发出的中断信号 作用
1. 周期性中断信号 (Periodic Interrupt:PF) 这种中断发生速度可以调节,最慢500ms(0.5秒)一次,最快30.517微妙一次
2. 更新周期结束中断(Update-ended Interrupt: UI) 实时时钟更新cmos时间结束时发送的中断。更新周期开关由寄存器B的第七位SET决定,0表示更新周期每秒都会发生,1表示终止当前更新周期,此后不再产生更新周期。寄存器第四位UIE,0表示不产生更新周期结束中断信号,1表示产生更新周期结束中断信号
3. 闹钟中断(AlarmInterrupt:AI ) 当实时时钟到达指定闹点时如果允许会产生闹钟中断。是否产生由寄存器B位5 AIE (Alarm Interrupt Enable) ,1允许,0不允许。

时基选择与分频器的修改:寄存器A用来控制时基选择和周期性中断发生的速率,位4、5、位6,控制时基选择,开机后bios初始位456位为010,选择32.768khz频率;位0-3 这4位用来做速率选择。

周期性中断是否允许发生还由寄存器B决定:PIE位0不允许发生周期性中断信号,为1允许发生周期性中断,如果寄存器A的第四位0000,寄存器B的PIE则自动置零

更新周期结束中断:

更新周期是否已经开始由寄存器A位7 UIP,只读的,0表示在488微秒内更新周期不会启动(此时访问cmos中的时间、日历,闹钟是安全的),1表示正处于更新周期或更新周期马上启动,如果寄存器B的SET位是0,分频电路正确配置时,更新周期每秒发生一次,更新周期在寄存器A的UIP位置1后的488微妙内开始,整个周期的完成时间不会多于19840微秒,在更新周期进行时,cmos的时间地址位(0x00-0x09)会脱离总线访问,防止冲突破坏

RTC电路发出的中断信号-中断类型的判断-寄存器C

寄存器C,有中断发生,位7=1,再根据位4,5,6检查是那种中断(1表示发生,0表示未发生),读取寄存器C,位7清零;低四位保留位始终是0。寄存器C只读,读操作会清零寄存器C。

寄存器B位 寄存器B含义
7位 0允许1禁止
6位 0禁止1允许
5位 0禁止1允许
4位 0禁止1允许
3位 目前不用了
2位 日期和时间的数据格式0cmos日期和数据用BCD编码,1cmos日期和时间采用二进制编码
1位 小时格式,0cmos用12小时制,1采用24小时制,用0是cmos0x04小时最高位7是0上午,1下午
0位 目前已经不用了

test指令: test r/m ,r/imm ;test有2个操作数,目的操作数可以使寄存器r或者内存地址,源操作数可以是寄存器或者立即数,长度要一样,与逻辑与功能基本一样,按位逻辑与操作,但是结果不保存,目的操作数不破坏,标志寄存器的OF=CF=0,ZF、SF\PF根据测试结果而定

test al, 0x08 ;测试al的位3是0还是1;0x08 = 0000 1000 ,al位3=0,结果00000000,ZF=1 ,位3=1,结果00001000,ZF=0

更新周期流程:

not指令:逐位求反指令,not r/m 例如 not dh,将每一位求反

8259芯片内部有中断服务寄存器Interrup Service Register: ISP,8位,每一位对应一个中断输入引脚,如果处理器响应了某个中断,8259芯片将相应bit置1,表明在处理相应中断,如果不清除,下一次的中断无法进行处理,因此再处理中断完成时,需要在程序中清除中断服务寄存器此位的状态,方法是向8259a芯片发送End of Interrupt: EOI 命令,代码是0x20,如果中断来自主片,只发EOI到主片,如果从从片来的中断,主从片都要发送EOI

iret指令:Interrupt Return ,中断返回指令,弹出寄存器,iret执行时栈顶位置必须平衡,否则ip,cs,标志寄存器错乱无法返回

hlt指令: 使CPU进入低功耗状态,直到用中断唤醒

显示时间的用户程序。

         ;文件说明:用户程序 
         ;创建日期: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 ax
      push bx
      push cx
      push dx
      push es     
  .w0:                  ;读取时间前要先判断RTC是否处于更新周期,查看寄存器A的7位 UIP 判断,0表示安全,1不安全
      mov al,0x0a                        ;阻断NMI。当然,通常是不必要的
      or al,0x80                        ;把al最高位置1,阻断NMI非屏蔽中断                    
      out 0x70,al                       ;通过0x70端口指定操作寄存器A
      in al,0x71                         ;通过0x71端口读寄存器A
      test al,0x80                       ;测试第7位UIP是否是1 
      jnz .w0                            ;以上代码对于更新周期结束中断来说 
                                         ;是不必要的 
      xor al,al
      or al,0x80
      out 0x70,al
      in al,0x71                         ;读RTC当前时间()
      push ax                           ;压栈保存秒

      mov al,2
      or al,0x80
      out 0x70,al
      in al,0x71                         ;读RTC当前时间()
      push ax

      mov al,4
      or al,0x80
      out 0x70,al
      in al,0x71                         ;读RTC当前时间()
      push ax

      mov al,0x0c                        ;寄存器C的索引。且开放NMI 
      out 0x70,al
      in al,0x71                         ;读一下RTC的寄存器C,否则只发生一次中断
                                         ;此处不考虑闹钟和周期性中断的情况 
      mov ax,0xb800
      mov es,ax

      pop ax
      call bcd_to_ascii
      mov bx,12*160 + 36*2               ;从屏幕上的12行36列开始显示

      mov [es:bx],ah
      mov [es:bx+2],al                   ;显示两位小时数字

      mov al,':'
      mov [es:bx+4],al                   ;显示分隔符':' ,mov byte [es:bx+4],':'
      not byte [es:bx+5]                 ;反转显示属性 ,每秒钟反转一次冒号的颜色

      pop ax
      call bcd_to_ascii
      mov [es:bx+6],ah
      mov [es:bx+8],al                   ;显示两位分钟数字

      mov al,':'
      mov [es:bx+10],al                  ;显示分隔符':'
      not byte [es:bx+11]                ;反转显示属性

      pop ax
      call bcd_to_ascii
      mov [es:bx+12],ah
      mov [es:bx+14],al                  ;显示两位小时数字

      mov al,0x20                        ;中断结束命令EOI 
      out 0xa0,al                        ;向从片发送 
      out 0x20,al                        ;0x20端口向主片发送 

      pop es
      pop dx
      pop cx
      pop bx
      pop ax

      iret

;-------------------------------------------------------------------------------
bcd_to_ascii:                            ;BCD码转ASCII
                                         ;输入:AL=bcd码
                                         ;输出:AX=ascii
      mov ah,al                          ;分拆成两个数字 
      and al,0x0f                        ;仅保留低4位 
      add al,0x30                        ;转换成ASCII 

      shr ah,4                           ;逻辑右移4位 
      and ah,0x0f                        
      add ah,0x30

      ret

;-------------------------------------------------------------------------------
start:
      mov ax,[stack_segment]            ;初始化栈段和数据段,指向用户程序自己的数据段和栈段
      mov ss,ax                         ;当处理器执行改变栈段寄存器ss的指令时,它会禁止任何中断在下一条指令完成期间
      mov sp,ss_pointer                 ;这条指令完成前禁止中断,因为中断会压栈栈寄存器cs,ip,标志寄存器的内容,处理中断过程,将来要出栈寄存器cs,ip,flags数据返回
      mov ax,[data_segment]
      mov ds,ax

      mov bx,init_msg                    ;显示初始信息,表明用户程序要开始安装中断向量 
      call put_string

      mov bx,inst_msg                    ;显示安装信息 
      call put_string

      mov al,0x70                       ; al作为被乘数
      mov bl,4                          ; bl 乘数
      mul bl                             ;al*bl,乘积在ax里, 计算0x70号中断在中断向量表IVT中的偏移
      mov bx,ax                         ; ax要用先保存值                          

      cli                                ;防止改动期间发生新的0x70号中断,IF清零,禁止处理器相应中断,防止修改地址到一半时有中断产生到错误地址执行

      push es                           ;先保存es
      mov ax,0x0000                     ;将段地址0x0000中断向量表起始地址传给es
      mov es,ax
      mov word [es:bx],new_int_0x70      ;写入新的中断处理过程的偏移地址。

      mov word [es:bx+2],cs              ;写入新的中断处理过程的段地址,至此0x70中断处理程序已经替换
      pop es                            ;从栈恢复es原始内容
                ;下面启用更新周期结束中断,每当rtc更新了cmos中的日期和时间后,将发出此中断,更新周期每秒一次,该中断每秒发生一次
      mov al,0x0b                        ;RTC寄存器B
      or al,0x80                         ;阻断NMI ,将al最高位置1,不改变其他位
      out 0x70,al
      mov al,0x12                        ;设置寄存器B 0001 0010,禁止周期性中断,周期性中断禁止,禁止闹钟中断,允许更新周期结束中断,禁止方波,bcd保存时间,24小时制存储
      out 0x71,al                        ;通过端口0x71,将设置发给寄存器B,新结束后中断,BCD码,24小时制 

      mov al,0x0c
      out 0x70,al                       ; 写入0x70,也打开了非屏蔽中断NMI
      in al,0x71                         ;读RTC寄存器C,复位未决的中断状态,如果不读取,不清零,以后同样中断不会产生

      in al,0xa1                         ;通过端口0xa1 读8259从片的IMR寄存器 
      and al,0xfe                        ;0xfe = 11111110 清除bit 0(此位连接RTC)
      out 0xa1,al                        ;写回从片中断屏蔽寄存器寄存器 

      sti                                ;设置标志寄存器重新开放中断 ,与cli指令对应

      mov bx,done_msg                    ;显示安装完成信息 
      call put_string

      mov bx,tips_msg                    ;显示提示信息
      call put_string

      mov cx,0xb800
      mov ds,cx
      mov byte [12*160 + 33*2],'@'       ;屏幕第12行,35列

 .idle:
      hlt                                ;使CPU进入低功耗状态,直到用中断唤醒
      not byte [12*160 + 33*2+1]         ;反转@显示属性 ,not不影响标志位
      jmp .idle

;-------------------------------------------------------------------------------
put_string:                              ;显示串(0结尾);输入:DS:BX=串地址
         mov cl,[bx]
         or cl,cl                        ;cl=0 ?
         jz .exit                        ;是的,返回主程序 
         call put_char
         inc bx                          ;下一个字符 
         jmp put_string

   .exit:
         ret

;-------------------------------------------------------------------------------
put_char:                                ;显示一个字符
                                         ;输入:cl=字符ascii
         push ax
         push bx
         push cx
         push dx
         push ds
         push es

         ;以下取当前光标位置
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;高8位 
         mov ah,al

         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;低8位 
         mov bx,ax                       ;BX=代表光标位置的16位数

         cmp cl,0x0d                     ;回车符?
         jnz .put_0a                     ;不是。看看是不是换行等字符 
         mov ax,bx                       ; 
         mov bl,80                       
         div bl
         mul bl
         mov bx,ax
         jmp .set_cursor

 .put_0a:
         cmp cl,0x0a                     ;换行符?
         jnz .put_other                  ;不是,那就正常显示字符 
         add bx,80
         jmp .roll_screen

 .put_other:                             ;正常显示字符
         mov ax,0xb800
         mov es,ax
         shl bx,1
         mov [es:bx],cl

         ;以下将光标位置推进一个字符
         shr bx,1
         add bx,1

 .roll_screen:
         cmp bx,2000                     ;光标超出屏幕?滚屏
         jl .set_cursor

         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
         mov bx,3840                     ;清除屏幕最底一行
         mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

         mov bx,1920

 .set_cursor:
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         mov al,bh
         out dx,al
         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         mov al,bl
         out dx,al

         pop es
         pop ds
         pop dx
         pop cx
         pop bx
         pop ax
         ret

;===============================================================================
SECTION data align=16 vstart=0
    init_msg       db 'Starting...',0x0d,0x0a,0
    inst_msg       db 'Installing a new interrupt 70H...',0
    done_msg       db 'Done.',0x0d,0x0a,0
    tips_msg       db 'Clock is now working.',0

;===============================================================================
SECTION stack align=16 vstart=0
                 resb 256
ss_pointer:
;===============================================================================
SECTION program_trail
program_end:

内部中断 :处理器检测到除数为0或者除法结果溢出,产生0号中断,遇到非法指令:指令操作码没定义、指令超过规定长度

软终端: int n ,n从0-255 ,8086只能识别0-255个中断号,机器操作码CD+一个字节的n,例如int 0x70, int3指令:陷阱中断 机器码是cc,用来调试设置断点

into 溢出中断指令,当处理器执行这个指令时,如果标志寄存器CE是1,则进入4号中断,否则什么都不做。

call 必须制定目标

int 可以用来调用系统程序,例如BIOS(中断功能)调用(键盘输入服务0x16,磁盘读写服务,文本图形显示服务)

mov ah, 0 int 0x16 0表示从键盘读字符,中断服务监视从键盘动作,中断返回会在al中存放键盘输入的asc码

使用中断在屏幕显示字符的用户程序:

         ;代码清单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,ax
      mov sp,ss_pointer
      mov ax,[data_segment]
      mov ds,ax
      ;从34到42使用bios中断显示字符串,用0x10中断的0x0e功能
      mov cx,msg_end-message
      mov bx,message

 .putc:
      mov ah,0x0e
      mov al,[bx]
      int 0x10
      inc bx
      loop .putc

 .reps:   ;使用软中断0x16 读键盘,00从键盘读,返回后al是字符编码
      mov ah,0x00
      int 0x16
            ;在显示字符
      mov ah,0x0e
      mov bl,0x07
      int 0x10

      jmp .reps

;===============================================================================
SECTION data align=16 vstart=0

    message       db 'Hello, friend!',0x0d,0x0a
                  db 'This simple procedure used to demonstrate '
                  db 'the BIOS interrupt.',0x0d,0x0a
                  db 'Please press the keys on the keyboard ->'
    msg_end:

;===============================================================================
SECTION stack align=16 vstart=0

                 resb 256
ss_pointer:

;===============================================================================
SECTION program_trail
program_end:
登陆评论: 使用GITHUB登陆