NASM(一)主引导扇区


计算机的启动过程

计算机的加电和复位

    在处理器的引脚中,有一个是RESET,用于接受复位信号。每当处理器加电,或者RESET引脚的电平由低变高时,处理器都会执行一个硬件初始化,以及一个可选的内部自测试(Build-in Self-Test,BIST),然后将内部所有寄存器的内容初始到一个预置的状态。

    对于Intel8086来说,复位将使代码段寄存器(CS)的内容设置为0xFFFF,其他所有寄存器的内容都为0x0000,包括指令指针寄存器(IP)。

    Intel8086可以访问1MB的内存空间,地址范围为0x00000到0xFFFFF,ROM占据着整个内存空间顶端的64KB,物理地址范围是0xF0000~0xFFFFF,里面固化了开机时要执行的指令;DRAM占据着较低端的640KB,地址范围是0x00000~0x9FFFF;中间还有一部分,分给了其他外围设备。

    因为8086加电或者复位时,CS=0xFFFF,IP=0x0000,因此它取的第一条指令位于物理地址0xFFFF0,正好位于ROM中,那里固化了开机时需要执行的指令。处理器取指令执行的自然顺序是从内存的低地址往高低地址推进。如果从0xFFFF0开始执行,这个位置离1MB内存的顶端(物理地址0xFFFFF)只有16个字节的长度,一旦IP寄存器的值超过0x000F,比如IP=0x0011,那么,它与CS一起形成的物理地址将因为溢出而变成0x00001,这将回绕到1MB内存的最低端。所以,ROM中位于物理地址0xFFFF0的地方,通常是一个跳转指令,它通过改变CS和IP的内容,使处理器从ROM中的较低地址处开始取指令执行。这块ROM芯片中的内容主要是进行硬件的诊断、检测和初始化。

主引导扇区

    处理器加电或者复位之后,如果硬盘是首选的启动设备,那么,ROM-BIOS将试图读取硬盘的0面0道1扇区。传统上,这就是主引导扇区(Main Boot Sector,MBR)。读取主引导扇区数据有512字节,ROM-BIOS程序将它加载到逻辑地址0x0000:0x7c00处,也就是物理地址0x07c00处,然后判断它是否有效。一个有效的主引导扇区,其最后两个字节应当是0x55和0xAA。ROM-BIOS程序首先检测这两个标志,如果主引导扇区有效,则以一个段间转移指令jmp 0x0000:0x7c00跳到那里继续执行。

编写主引导扇区代码

    8086可以访问1MB内存。其中,0x00000~9FFFF属于常规内存,由内存条提供;0xF0000~0xFFFFF由主板上的一个芯片提供,即ROM-BIOS。中间还有一个320KB的空洞,即0xA0000~0xEFFFF。传统上,这段地址空间由特定的外围设备来提供,其中就包括显卡。一直以来0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。由于历史的原因,所有在个人计算机上使用的显卡,在加电自检之后都会把自己初始化到80×25的文本模式。在这种模式下,屏幕上可以显示25行,每行80个字符,每屏总共2000个字符。

    在不同设备之间,或者在同一设备的不同模块之间有一个信息传递标准是非常必要的。屏幕上的每个字符对应着显存中的两个连续字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。字符的显示属性(1字节)分为两部分,低4位定义的是前景色,高4位定义的是背景色。色彩主要由R、G、B这3位决定,我们知道,可以由红(R)、绿(G)、蓝(B)三原色来配出其他所有颜色。K是闪烁位,为0时不闪烁,为1时闪烁;I是亮度位,为0时正常亮度,为1时呈高亮。

KRGBIRGB

显示字符(一)

main:
    ; 显示HELLO
    mov ax, 0xb800
    mov es, ax
    mov byte [es:0x0000], 'H'
    mov byte [es:0x0001], 0x07
    mov byte [es:0x0002], 'E'
    mov byte [es:0x0003], 0x07
    mov byte [es:0x0004], 'L'
    mov byte [es:0x0005], 0x07
    mov byte [es:0x0006], 'L'
    mov byte [es:0x0007], 0x07
    mov byte [es:0x0008], 'O'
    mov byte [es:0x0009], 0x07
    
    ; 显示number的地址
    mov cx, cs
    mov ds, cx ; 代码段与数据段指向同一地址
    
    mov ax, 12345 ; 设置被除数
    mov bx, 10 ; 设置除数
    mov dx, 0 ; 高16位置0
    div bx ; 除法操作
    mov [0x7c00+number+0x00],dl   ;保存个位上的数字
    
    xor dx,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   ;保存万位上的数字
    
    ; 显示数字
    mov al,[0x7c00+number+0x04]
    add al,0x30
    mov [es:0x0a],al
    mov byte [es:0x0b],0x04
    
    mov al,[0x7c00+number+0x03]
    add al,0x30
    mov [es:0x0c],al
    mov byte [es:0x0d],0x04
    
    mov al,[0x7c00+number+0x02]
    add al,0x30
    mov [es:0x0e],al
    mov byte [es:0x0f],0x04
    
    mov al,[0x7c00+number+0x01]
    add al,0x30
    mov [es:0x10],al
    mov byte [es:0x11],0x04
    
    mov al,[0x7c00+number+0x00]
    add al,0x30
    mov [es:0x12],al
    mov byte [es:0x13],0x04
    
; 数据段
number:
    db 0,0,0,0,0
    
    jmp near main
    times 510-($-$$) db 0
    db 0x55
    db 0xaa
    
  • main:处理器访问内存时,采用的是段地址:偏移地址的模式。对于任何一个内存段来说,段地址可以开始于任何16 字节对齐的地方,偏移地址则总是从0x0000开始递增。 为了支持这种内存访问模式,在源程序的编译阶段,编译器会把程序整体上作为一个独立的段来处理,并从0开始计算和跟踪每一条指令的地址。因为该地址是在编译期间计算的,故称为汇编地址。在NASM汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令的汇编地址。行首带冒号的是标号是main,main就指代地址0x0000。
  • mov指令用于数据传送。mov dst, src

    • dst:必须是通用寄存器或者内存单元。这里使用段超越前缀ES,要求处理器在生成物理地址时,使用段地址寄存器ES。ES的值已经在前面被设为0xB800,故它指向ES段中,偏移地址为0的内存单元,即0xB800:0x0000,也就是物理地址0xB8000,这个内存单元对应着屏幕左上角第一个字符的位置。目的操作数必须用方括号围起来,以表明它是一个地址,处理器应该用这个地址再次访问内存,将源操作数写进这个单元。
    • src:可以是和目的操作数具有相同数据宽度的通用寄存器和内存单元,也可以是立即数。多数汇编语言编译器允许在指令中直接使用字符的字面值来代替数值形式的ASCII码。
    • 传送指令只影响目的操作数的内容,不改变源操作数的内容。
    • dst不允许为立即数,并且srcdst不允许同时为内存单元。
    • 使用关键字byteword来修饰目的操作数,指出本次传送是以字节或字的方式进行的。
  • div指令用于做除法。计算机不能直接显示12345这样的数字,我们需要将其每一位计算出来,再将每一位数字转换为ASCII码,再显示到屏幕上。我们使用除法求余和商来计算每一位数字。8086 处理器提供了除法指令div,它可以做两种类型的除法。这里使用的是第二种。

    • 第一种类型是用16位的二进制数除以8位的二进制数。在这种情况下,被除数必须在寄存器AX中,必须事先传送到AX寄存器里。除数可 以由8 位的通用寄存器或者内存单元提供。指令执行后,商在寄存器AL中,余数在寄存器AH中。
    • 第二种类型是用32位的二进制数除以16位的二进制数。在这种情况下,因为16位的处理器无法直接提供32位的被除数,故要求被除数的高16位在DX中,低16位在AX中 。指令执行后,商在AX中,余数在DX中。
  • add指令需要两个操作数,目的操作数可以是8位或者16位的通用寄存器,或者指向8位或者16位实际操作数的内存地址;源操作数可以是相同数据宽度的8位或者16位通用寄存器、指向8位或者16位实际操作数的内存地址,或者立即数,但不允许两个操作数同时为内存单元。相加后,结果保存在目的操作数中。
  • DB:声明字节(Declare Byte),跟在它后面的操作数都占一个字节的长度(位置)。如果要声明超过一个以上的数据,各个操作数之间必须以逗号隔开。它们的位置在声明的汇编地址处。其他声明数据的指令还有

    • DW:DW(Declare Word)用于声明字数据。
    • DD:DD(Declare Double Word)用于声明双字(两个字)数据
    • DQ:DQ(Declare Quad Word)用于声明四字数据。

    DBDWDDDQ并不是处理器指令,它只是编译器提供的汇编指令,所以称做伪指令。伪指令是汇编指令的一种,它没有对应的机器指令,所以它不是机器指令的助记符,仅仅在编译阶段由编译器执行。

  • jmp是转移指令,用于使处理器脱离当前的执行序列,转移到指定的地方执行,关键字near表示目标位置依然在当前代码段内。

    • jmp指令有多种格式。最典型地,它的操作数可以是直接给出的段地址和偏移地址,这称为绝对地址。
    • jmp 指令使用了关键字near,后接的操作数并非目标位置的偏移地址,而是目标位置相对于当前指令处的偏移量(以字节为单位)。在编译阶段,编译器用标号(目标位置)处的汇编地址减去当前指令的汇编地址,再减去当前指令的长度3,就得到了jmp near指令的实际操作数。
  • 一个有效的主引导扇区,其最后两个字节的数据必须是0x55 和0xAA。我们使用伪指令db实现
  • 伪指令times 可用于重复它后面的指令若干次。$表示当前指令的地址,$$表示程序的起始地址,所以$-$$就等于本条指令之前的所有字节数。填充了这些0之后,从程序开始到最后一个0,一共是510个字节。再加上最后的DB两个字节,即0xaa55结束标志,整段程序的大小就是512个字节,刚好占满一个扇区。

显示字符(二)

在显示字符(一)中,数据之间的传输不是批量传输的,在显示字符(二)中,数据将进行批量传输并实现相同的功能。

main:
    ; 显示字符
    mov ax, 0x7c0 ; 设置段基址为0x7c0,取地址时段基址会左移四位加上偏移地址
    mov ds, ax
    mov ax, 0xb800
    mov es, ax
    cld
    mov si, info
    mov di, 0
    mov cx, number - info
    rep movsb
    ; 开始做除法,分解数位
    mov cx, 5 ; 除法次数
    mov bx, number ; 保存地址
    mov ax, 12345 ; 被除数
    mov si, 10 ; 除数
digit:
    mov dx, 0
    div si
    mov [bx], dl
    inc bx
    loop digit
    ; 显示数据
    mov bx, number 
    mov si, 4
show:
    mov al, [bx + si]
    add al, 0x30
    mov ah,0x04
    mov [es:di], ax
    add di, 2 ; 指向显示缓冲区的下一个单元。
    dec si ; si减一,处理下一位
    jns show
info:
    db 'H',0x07,'E',0x07,'L',0x07,'L',0x07,'O',0x07,':',0x07,\
       'W',0x07,'O',0x07,'R',0x07,'L',0x07,'D',0x07,
number:
    db 0,0,0,0,0
    jmp near $
    times 510-($-$$) db 0
    db 0x55
    db 0xaa
  • movsbmovsw指令执行时,原始数据串的段地址由DS指定,偏移地址由SI指定,简写为DS:SI;要传送到的目的地址由ES:DI指定;传送的字节数(movsb)或者字数(movsw)由CX指定。除此之外,还要指定是正向传送还是反向传送,正向传送是指传送操作的方向是从内存区域的低地址端到高地址端;反向传送则正好相反。正向传送时,每传送一个字节(movsb)或者一个字(movsw),SI和DI加1或者加2;反向传送时,每传送一个字节(movsb)或者一个字(movsw)时,SI和DI减去1或者减去2。不管是正向传送还是反向传送,也不管每次传送的是字节还是字,每传送一次,CX的内容自动减一。
  • 在8086处理器里,有一个特殊的寄存器,叫做标志寄存器FLAGS,它的第10位是方向标志DF(Direction Flag),通过将这一位清零或者置1,就能控制movsbmovsw的传送方向。
  • 方向标志清零指令是cld。这是个无操作数指令,与其相反的是置方向标志指令stdcld指令将DF标志清零,以指示传送是正方向的。和cld功能相反的是std指令,它将DF标志置位1。
  • 指令前缀rep(repeat),意思是CX不为零则重复。若指令为repmovsb,则坐落movsb将重复执行直到CX的内容为零。
  • loop指令的功能是重复执行一段相同的代码,处理器在执行它的时候会顺序做两件事:

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

    为了使loop指令能正常工作,我们需将循环次数传送到CX寄存器。

  • mov [bx],dl中,我们将数据写入以BX的内容为偏移地址的内存单元中去,在8086处理器上,如果要用寄存器来提供偏移地址,只能使用BX、SI、DI、BP,不能使用其他寄存器,因为每个寄存器有它自己的作用,如:

    • AX是累加器(Accumulator),与它有关的指令还会做指令长度上的优化(较短)
    • CX是计数器(Counter)
    • DX是数据(Data)寄存器,除了作为通用寄存器使用外,还专门用于和外设之间进行数据传送
    • SI是源索引寄存器(Source Index)
    • DI是目标索引寄存器(Destination Index),用于数据传送操作
  • inc是加一指令,操作数可以是8位或者16位的寄存器,也可以是字节或者字内存单元。从功能上说,它和addbx,1作用相同,但它更快。inc指令相对的是dec指令,用于将目标操作数的内容减一。
  • moval,[bx+si]中,由于INTEL8086处理器只允许以下几种基址寄存器和变址寄存器的组合:[bx+si][bx+di][bp+si][bp+di]。这些组合可以用于任何带有内存操作数的指令中。其他任何组合都是非法的。
  • jns show的意思是,如果未设置符号位,则转移到标号show所在的位置处执行。Intel处理器的标志寄存器里有符号位SF(Sign Flag),如果计算结果的最高位是比特0,处理器把SF位置0,否则SF位置1。

从1加到100

main:
    mov ax, 0x7c0
    mov ds, ax
    mov ax, 0xb800
    mov es, ax

    mov si, msg
    mov di, 0
    mov cx, endmsg - msg
    
move_msg: ; 将信息显示到屏幕
    mov al, [ds:si]
    mov [es:di], al
    inc di
    mov byte [es:di], 0x07
    inc di
    inc si
    loop move_msg
    ; 计算1+2+3+...+100
    mov cx, 1
    mov ax, 0
calc:
    add ax, cx
    inc cx
    cmp cx, 100
    jle calc
    ; 准备显示数据
show:
    mov cx, 0
    mov ss, cx
    mov sp, cx
    mov bx, 10
    ; 压栈操作,存储分解好的位
show_push:
    mov dx, 0
    div bx
    add dl, 0x30
    push dx
    inc cx
    cmp ax, 0
    jne show_push
    ; 显示数据
show_pull:
    pop dx
    mov [es:di], dl
    inc di
    mov byte [es:di], 0x07
    inc di
    loop show_pull
msg:
    db '1+2+3+4+...+100='
endmsg:

    jmp near $
    times 510-($-$$) db 0
    db 0x55
    db 0xaa
  • cmp指令:比较指令,它需要两个操作数,目的操作数可以是8位或者16位通用寄存器,也可以是8位或者16位内存单元;源操作数可以是与目的操作数宽度一致的通用寄存器、内存单元或者立即数,但两个操作数不能同时为内存单元。cmp指令在功能上和sub指令相同,唯一不同之处在于,cmp指令仅仅根据计算的结果设置相应的标志位,而不保留计算结果,因此也就不会改变两个操作数的原有内容。cmp指令将会影响FLAG寄存器中状态标志位。对cmp ax,bx来说AX是被测量的对象,BX是测量的基准。cmp指令通常和条件转移指令配合使用
  • FLAG寄存器:设计为16位,实际使用9位,其中6位用以存放算术逻辑单元运算后的结果特征,称为状态标志;另外3位通过人为设置,用以控制8086的三种特定操作,称为控制标志

    • 状态标志

      • 进位标志CF:用于反映运算是否产生进位或借位。如果运算结果的最高位产生一个进位或借位,则CF置1,否则置0。运算结果的最高位包括字操作的第15位和字节操作的第7位。移位指令也会将操作数的最高位或最低位移入CF。
      • 奇偶标志PF:用于反映运算结果低8位中“1”的个数。“1”的个数为偶数,则PF置1,否则置0。
      • 辅助进位标志AF:算数操作结果的第三位(从0开始计数)如果产生了进位或者借位则将其置为1,否则置为0,常在BCD(binary-codedecimal)算术运算中被使用。
      • 零标志ZF:用于判断结果是否为0。运算结果0,ZF置1,否则置0。
      • 符号标志SF:用于反映运算结果的符号,运算结果为负,SF置1,否则置0。因为有符号数采用补码的形式表示,所以SF与运算结果的最高位相同。
      • 溢出标志OF:反映有符号数加减运算是否溢出。如果运算结果超过了8位或者16位有符号数的表示范围,则OF置1,否则置0。
    • 控制标志

      • 跟踪标志TF:当TF被设置为1时,CPU进入单步模式,所谓单步模式就是CPU在每执行一步指令后都产生一个单步中断。主要用于程序的调试。8086/8088中没有专门用来置位和清零TF的命令,需要用其他办法。
      • 中断标志IF:决定CPU是否响应外部可屏蔽中断请求。IF为1时,CPU允许响应外部的可屏蔽中断请求。
      • 方向标志DF:决定串操作指令执行时有关指针寄存器调整方向。当DF为1时,串操作指令按递减方式改变有关存储器指针值,每次操作后使SI、DI递减。
  • 条件转移指令

    比较结果英文描述指令相关标志位的状态
    等于Equalje相减结果为零才成立,故要求ZF= 1
    不等于Not Equaljne相减结果不为零才成立,故要求ZF=0
    大于Greaterjg适用于有符号数比较,要求: ZF=0 (两个数不同,相减的结果不为零),并且SF=OF(如果相减后溢出,则结果必须是负数,说明目的操作数大:如果相减后未溢出,则结果必须是正数,也表明目的操作数大些)
    大于等于Greater or Equaljge适用于有符号数的比较,要求:SF=OF
    不大于Not Greaterjng适用于有符号数的比较要求: ZF=1 (两个数相同,相减的结果为零),或者SF≠OF(如果相减后溢出,则结果必须是正数,说明源操作数大:如果相减后未溢出,则结果必须是负数,同样表明源操作数大些)
    不大于等于Not Greater or Equaljnge适用于有符号数的比较,要求: SF≠OF
    小于Lessjl适用于有符号数的比较,等同于“不大于等于”,要求: SF≠OP
    小于等于Less or Equaljle适用于有符号数的比较,等同于“不大于”要求: ZF=1 (两个数相同,相减的结果为零),并且SF≠OF(如果相减后溢出,则结果必须是正数,说明源操作数大:如果相减后未溢出,则结果必须是负数,同样表明源操作数大些)
    不小于Not Lessjnl适用于有符号数的比较,等同于“大于等于”,要求: SF=OF
    不小于等于Not Less or Equaljnle适用于有符号数的比较,等同于“大子”,要求: ZF=0 (两个数不同,相减的结果不为零),并且SF=OF(如果相减后溢出,则结果必须是负数,说明日的操作数大;如果相减后未溢出,则结果必须是正数,也表明目的操作数大些)
    高于Aboveja适用于无符号数的比较,要求: CF=0 (没有进位或借位)而且ZF=0 (两个数不相同)
    高于等于Above or Equaljac适用于无符号数的比较,要求: CF=0 (目的操作数大些,不需要借位)
    不高于Not Abovejna适用于无符号数的比较,等同于“低于等于”(见后),要求: CF=1或者ZF=1
    不高于等于Not Above or Equaljnae适用于无符号数的比较,等同于“低于”(见后),要求: CF=1
    低于Belowjb适用于无符号数的比较,要求: CF=1
    低于等于Below or Equaljbe适用于无符号数的比较,要求: CF=1或者ZF=1
    不低于Not Belowjnb适用于无符号数的比较,等同于“高于等于”,要求: CF=0
    不低于等于Not Below or Equaljnbe适用于无符号数的比较,等同于“高于”,要求: CF=0而且ZF=0
    校验为偶Parity Evenjpe要求: PF=1
    检验为奇Parity Oddjpo要求: PF=0
  • 压栈和出栈指令:压栈和出栈只能在一端进行,所以需要用栈指针寄存器SP(StackPointer)来指示下一个数据应当压入栈内的什么位置,或者数据从哪里出栈。

    • 在此段代码中,代码段和栈段是同一个段,所以段寄存器CS和SS的内容都是0x0000。栈指针寄存器SP的内容在源程序中被置为0。所以,当push指令第一次执行时,SP的内容减2,即0x0000-0x0002=0xFFFE,借位被忽略。于是,被压入栈的数据,在内存中的位置实际上是0x0000:0xFFFE。以后每次压栈时,SP都要依次减2。
    • pop指令执行时,处理器将栈段寄存器SS的内容左移4位,再加上栈指针寄存器SP的内容,形成20位的物理地址访问内存,取得所需的数据。然后,将SP的内容加操作数的字长,以指向下一个栈位置。
    • pop指令和push指令的操作数是字,且Intel处理器是使用低端字节序的,故低字节在低地址部分,高字节在高地址部分,正好占据了栈段的最高两个字节位置。
    • pop指令和push指令不影响任何标志位。

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - NASM(一)主引导扇区


我的朋友,理论是灰色的,而生活之树是常青的!