计算机语言 x86汇编语言:从实模式到保护模式(操作系统引导课) 原书作者李忠
用电表示数据
寄存器的作用:具有记忆功能的器件。锁存可以通过下面的开关控制,平时开关为空,按下开关之后,将输入锁存起来。锁存之后右面灯泡就就不会变化了,除非再次按下锁存开关。下面的开关是关着的,有间隙,是上下按的。
带有寄存器的加法机如何工作:每次输入/数字后存入寄存器,而不是多组线进行输入。
再入加减乘除就是多了其他控制开关,
上面的加减乘除是每个开关,其他控制的地方也可以用一组01值来表示各类操作。
内存
下面每个方块是一个内存单元,用右面地址线01值组合后来代表要查找的哪个内存单元。主流计算机里每个内存单元为8bit。
内存上还有另外一排数据线,即内存的数据,8bit。读写通过控制线来表示。
经过改进之后的计算机器,变为了下图。
指令指针寄存器,用来表示指令的地址,刚开始的时候是下一条要执行的地址。
那么内存中的指令可以看为
然后就可以一条一条自动取出计算了。
处理器
CPU发展史:4004–8008(8位)–8086(16位)–80286–80386(i386)
16位代表有16位的寄存器和16位的算术逻辑部件。
- 指令集:可以执行的指令,在出产的时候就已经决定了。
8086内部的通用寄存器
有8个通用的寄存器,分别是下面,他们都是16bit的,每个可以分为H/L
1字=2字节。
数据线的宽度和寄存器的宽度一致,是读取内存单元的。
程序在内存中如何分段存放。
程序重定位困境
上图中,比如将程序挪到位置了,那么原来程序中的表示地址的内容将不再使用,解决方法是让表示地址从绝对地址变为相对于程序段开始的offset。
IPR是地址寄存器,DSR是数据段寄存器。
8086有20根地址线,但是寄存器都是16位的,IP寄存器不够用,所以需要另外一个寄存器CS,所以我们一遍表示地址是CS:IP,CS是高位,所以是需要乘上10倍(实际上是16倍,2的4次方,4个偏移)与IP相加的。
上面所说的是8086的策略,
8086的段地址和偏移地址的访问过程就变成了:
执行完1个指令后,IP就会变为03。CS代表是当前代码段的地址表示,IP代表段内的offset。
因为IP是16位的,就代表段的最大大小为64k,即2的16次方,
汇编语言基础指令
- 大小写不敏感
- 3FH和3fh是一样的,H代表是16位。63代表是十进制的,0011B代表是二进制的
mov a,b ; 将b的内存放到a中
add a,b ; a=a+b
安装notepad,nasm,
https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/
NASM的作用是将mov等指令编译成二进制文件。
test.asm
mov bx,0x3f; 16进制,将立即数放到bx中,立即数代表的是内存地址里存放的内容
add ax,bx; 注释
;nasm test.asm -f bin -o test.bin ; -f输出文件的格式,只有二进制内容 -o 输出的文件名
下面显示案例:每一行是16B
如何将NASM编译二进制功能继承到notepad++:
视频用户注意:在刚开始录制配套视频时,尚未编写nasmide2.exe,所以视频中推荐使用Notepad++,现在已经不推荐使用。不排除某些读者朋友坚持使用这个软件,在这种情况下,您可以按如下方法进行配置,配置后将可以提供自动编译过程:
1,启动NotePad++,在菜单上选择“运行(R)”->“运行(R)”。
2,在弹出的窗口内,输入:cmd /k pushd "$(CURRENT_DIRECTORY)" & D:\ACERFILES\OLDE\PROGFILES\NASM\nasm.exe -f bin "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT。其中,“D:\ACERFILES\OLDE\PROGFILES\NASM\”应该改成你自己机器上实际的NASM安装路径。
cmd /k pushd "$(CURRENT_DIRECTORY)" & E:\NASM\nasm.exe -f bin "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT
3,点击“保存”,然后为这个运行命令起一个名字并分配一个快捷键,这样你下次就可以直接快速执行编译过程。
计算机启动,8086寄存器的状态
计算机启动后,寄存器被强制变为下面的图
可以看到CS:IP=FFFF:0000=FFFF0,可以看到距离最上面只有16B,这个是bios,所以还要继续跳转。
8086的地址范围:
从上到下
- 内存
- DRAM动态随机访问存储器
- 外接设备
- 显卡:用来转换成视频信号给显示器
- ROM BIOS,read only memory,只读的,厂家烧制好的程序
但是FFFF0是怎么调到别的地方呢,是使用JMP命令
也就是又调到了0xF000:E05B
硬盘
- USE
- HDD:有机器转盘
- SSD:集成电路硬盘
可能会有多个盘
每一个圆圈是一个磁道
磁道还可以分为扇区,每个扇区大小相等,扇区从1开始编号,其他内容是从0开始编号
编号从柱面外面开始,先将一个柱面填满后再一定到另一个柱面。
主引导扇区
主引导扇区代表是0柱面,0磁道,第一个扇区就是主引导扇区。
如果是从硬盘启动,那么就会跳转到07c00处开始,读取硬盘主引导扇区的内容加载到07c00处,然后调到这里接着执行。JMP 07c00也是bios执行的最后一条指令。
虚拟硬盘
每个厂商的虚拟硬盘格式不同,
工具包有个LIZHONG.vhd,他也是一个文件,规范里写明了vhd的内容应该这么组织才算是一个正确的vhd:
- 文件末尾必须是。。。
- 512字节
- 55aa
debug功能,可以使用bochs虚拟机
bochs是一台虚拟机,对应的配置都是通过配置文件来配置的,
如何创建固定尺寸的VHD虚拟硬盘,放到bochs上,可以去virtualBox中利用一下,选择固定大小,20M就够
双击
然后找到这个地方填充,还需要填充磁道之类的,这个信息需要使用工具生成,
填充磁盘内容
mov ax,0x30;
moc dx,0xc0
add ax,bx
times 502 db 0; 502+2+
db 0x55
db 0xAA
LBA,
00000000000i[PLUGIN] reset of 'serial' plugin device by virtual method
00000000000i[PLUGIN] reset of 'gameport' plugin device by virtual method
00000000000i[PLUGIN] reset of 'iodebug' plugin device by virtual method
00000000000i[PLUGIN] reset of 'usb_uhci' plugin device by virtual method
00000000000i[ ] set SIGINT handler to bx_debug_ctrlc_handler
Next at t=0
# 要跳转了
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
<bochs:1> sreg ;;;;;;;;;
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000000000, limit=0xffff
idtr:base=0x0000000000000000, limit=0xffff
<bochs:2> r
rax: 00000000_00000000
rbx: 00000000_00000000
rcx: 00000000_00000000
rdx: 00000000_00000000
rsp: 00000000_00000000
rbp: 00000000_00000000
rsi: 00000000_00000000
rdi: 00000000_00000000
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_0000fff0
eflags 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf
; 单步执行为s b为打断点 c为继续执行到下一断点
每个位数的处理器上表示通用寄存器的方式不同
00000000000i[PLUGIN] reset of 'gameport' plugin device by virtual method
00000000000i[PLUGIN] reset of 'iodebug' plugin device by virtual method
00000000000i[PLUGIN] reset of 'usb_uhci' plugin device by virtual method
00000000000i[ ] set SIGINT handler to bx_debug_ctrlc_handler
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
<bochs:3> s
Next at t=1
(0) [0x0000000fe05b] f000:e05b (unk. ctxt): xor ax, ax ; 31c0
<bochs:4> s
Next at t=2
(0) [0x0000000fe05d] f000:e05d (unk. ctxt): out 0x0d, al ; e60d
<bochs:5> b 0x7c00
#
<bochs:6> c
....
00001771813i[BIOS ] ata0-0: PCHS=301/4/17 translation=none LCHS=301/4/17
00005205777i[BIOS ] IDE time out
00017178813i[BIOS ] Booting from 0000:7c00
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=17178868
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0030 ; b83000
选择CD-ROM继续,
Next at t=17178868
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, 0x0030 ; b83000
<bochs:7> r
rax: 00000000_0000aa55 ; 执行吓一跳指令后变为0030
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
rbp: 00000000_00000000
rsi: 00000000_000e0000
rdi: 00000000_0000ffac
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_00007c00
eflags 0x00000082: id vip vif ac vm rf nt IOPL=0 of df if tf SF zf af pf cf
(0) [0x000000007c03] 0000:7c03 (unk. ctxt): mov dx, 0x00c0 ; bac000
<bochs:9> r
rax: 00000000_00000030
rbx: 00000000_00000000
。。。
<bochs:12> s
Next at t=17178870
(0) [0x000000007c06] 0000:7c06 (unk. ctxt): add ax, bx ; 01d8
<bochs:13> s
Next at t=17178871
(0) [0x000000007c08] 0000:7c08 (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000
显卡和显存
为了显示文件,需要显卡,
把显存映射到计算机可以访问的地址空间内,B8000
-BFFF可以映射。
MOV ax, 0xb800
mov语法
mov 目的操作数/寄存器/内存地址, 源操作数/寄存器/立即数
mov 段寄存器, 通用寄存器
mov 段寄存, [内存地址] ; 合法
mov ds,0xb800;不合法,不支持立即数直接传送到段寄存器
mov [0x00],al ; 按字节操作
mov ax, [0x02]; 按字操作,将0x02里存储的一个字放到ax寄存器中
moc ax, 0x02
mov byte [0x02], 0xe9
mov word [0x06], 0x3c
mov 0x07,al;错误的
mov [0x01],[0x02];错误的,不支持,其他操作也不支持这种类似的操作
mov ip,0x01;指令存储寄存器不能直接访问,不可以出现在任何指令中
mov ds,ax
mov es,[0x01]
- 可以使用start标识离那条最近指令的地址
start:
mov ax,0x41
....
; 在debug时,不应该执行下面的无效指令,所以后面教程会改成jmp到别的地方运行 ;
; jmp
current:
times 510-(current-start) db 0
db 0x55,0xaa
AS ;代码清单6-1
;文件名:c06_mbr.asm ; nasm test.asm -l test.lst
;文件说明:硬盘主引导扇区代码
;创建日期:2011-3-31 21:15,修订于2021-09-06 23:05
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
;设置数据段的基地址
mov cx,cs
mov ds,cx
;求个位上的数字
mov dx,0
div bx
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 ;保存万位上的数字
;以下用十进制显示标号的偏移地址
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'
mov byte [es:0x25],0x07
infi: jmp near infi ;无限循环
number db 0,0,0,0,0
times 203 db 0 ;
db 0x55,0xaa
视频用户注意:在刚开始录制配套视频时,尚未编写nasmide2.exe,所以视频中推荐使用Notepad++,现在已经不推荐使用。不排除某些读者朋友坚持使用这个软件,在这种情况下,您可以按如下方法进行配置,配置后将可以提供自动编译过程:
1,启动NotePad++,在菜单上选择“运行(R)”->“运行(R)”。
2,在弹出的窗口内,输入:cmd /k pushd "$(CURRENT_DIRECTORY)" & D:\ACERFILES\OLDE\PROGFILES\NASM\nasm.exe -f bin "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" & PAUSE & EXIT。其中,“D:\ACERFILES\OLDE\PROGFILES\NASM\”应该改成你自己机器上实际的NASM安装路径。
cmd /k pushd "$(CURRENT_DIRECTORY)" & F:\LEEZHONG\NASM\nasm.exe -f bin "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).lst" & PAUSE & EXIT
3,点击“保存”,然后为这个运行命令起一个名字并分配一个快捷键,这样你下次就可以直接快速执行编译过程。
cmd /k pushd "$(CURRENT_DIRECTORY)" & F:\LEEZHONG\NASM\nasm.exe -f bin "$(FULL_CURRENT_PATH)" -o "$(NAME_PART).bin" -l "$(NAME_PART).lst" & PAUSE & EXIT
上述命令不用配置了,直接打开这个文件后点击编译文件,就在该目录里得到bin和lst了。
xp /512xb 0x7c00
b 0x7c00
u/32
把显存映射到计算机可以访问的地址空间内,
B8000
-BFFF可以映射。MOV ax, 0xb800 mov ds,ax
在virtualBox中移除原来的存储disk,挂载我们自己创建的。
div语法
简言之就是为了存储商和余数,有时候需要使用2个寄存器进行操作。
add语法
显示数字
附加段寄存器ES
段超越前缀
段超越前缀用来改变默认段寻址,通常内址寻址是数据段或者堆栈段,但你可以在指令前面加上段超越前缀,就可以访问到其它段内的数据。
8086/8088指令系统中的段地址有四个:
ES Extra segment 附加段寄存器
DS Data segment 数据段寄存器
CS Code segment 代码段寄存器
SS Stack segment 堆栈段寄存器
我们通常用到的寄存器间接寻址方式会用到下边几个
DI, SI, BX, BP
其中前三个对应的段默认位DS,就是数据段寄存器
而最后一个BP默认对应的是SS, 就是堆栈段寄存器
- 有效地址EA存放在BX、SI、DI或BP中,EA为BX、SI、DI时,默认是DS,BP默认是SS,可以使用段超越前缀改变。
所以当我们要用到代码段寄存器或者附加段寄存器的时候就会用到段超越前缀
例如:
mov ax, [si] = mov ax, ds:[si]
mov ax, [bp] = mov ax, ss:[bp]
而段超越的则必须在前边加上段地址:
mov ax, cs:[si]
; 如果不加 [es:0x00],则段地址默认是在ds中,使用了段超越前缀后,段地址是在指定的段寄存器里
虚拟机挂载的硬盘变为我们的硬盘。