《x86汇编语言:从实模式到保护模式》读书笔记
1.低端字节序列:高字节位于高地址部分,低字节位于低地址部分。
2.将一个16位的寄存器当成8位的寄存器来用时,对其中一个8位寄存器的操作不会影响到另一个8位寄存器。
也就是说,当你操作寄存器al时,不会影响到ah中的内容。
3.nasm -f bin exam.asm -o exam.bin
-f bin就是要求nasm生产的文件只包含“纯二进制”的内容。这样一来,因为缺少操作系统所需要的加载和冲定位
信息,他很难在windows,dos,linux上作为一个普通的应用程序运行。
4.intel的处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:
mov 段寄存器, 通用寄存器
mov 段寄存器,内存单元
5.8086内部有8个16位的通用寄存器,ax,bx,cx,dx,si,di,bp,sp;前面四个寄存器中的每一个,都可以当成两个8位的寄存器来使用。
6.20位的物理地址需要用无法用一个16位的寄存器来保存,所以我们有了段地址:偏移地址。
7.屏幕上的每个字符都对应着显存中的两个连续的字节,前一个是字符的ascll代码,后面是字符的显示属性.包括字符的颜色(前景色),和底色(背景色)。
字符的显示属性(一字节)分为两部分,低4定义的是前景色,高4位定义的是背景色。
8.mov byte [0x00],‘L’ 关键字byte用来修饰目的操作数,指出本次传送的是以字节的方式进行的。'L’可以解释成 0x4c和0x004c
所以编译器无法搞懂真实的意图,只能报告错误,所以必须用"byte"或者"word"进行修饰。
下面的指令不需要任何修饰
mov [0x00],al
mov al,[0x02]
9. 颜色 KRGB IRGB KRGB代表的是字符的颜色,后面的代表的是背影的颜色,K代表是否闪烁,I代表是否高亮
10.mov ax,bl 是不对的,因为宽度是不一样的。
11.如果有寄存器操作数,那么就不需要byte,和word修饰。
12.处理器不允许在两个内存单元直接进行传送操作。
13.当我们在日常使用的个人计算机上,文本模式下的显示缓冲区被映射到物理内存地址空间,其实地址为0xb8000.他对应的段地址为0xb800。
14.mov [0x06],0x55aa. 错误,错误的原因是没有用word或者tyte进行修饰。
15.在编译阶段,每条指令都被计算并赋予了一个汇编地址,就像他们已经被加载到内存中的某个段里一样,当编译好的程序加载到
物理内存后,他在段内的偏移地址和他在编译阶段的汇编地址是相同的。
16.在nasm汇编语言里面,每条指令的前面都可以拥有一个标号。
infi: jmp near infi 在这里,行首带冒号的标号是infi,这条指令的汇编地址是0x0000012B,所以infi就代表数值0x0000012B,或者说是这个符号的
符号化表示。
infi jmp near infi 这样写也是可以的,冒号并不是必须的
infi :
jmp near infi 因为infi所在的那一行没有指令,他的地址就是下一行的地址,换句话说,和下一行的地址是相同的
17.要放在程序中的数据用DB指令来声明的,DB的意思是声明字节(Declare Byte),所以,跟在他后面的操作数都占一个字节的长度(位置)。
注意:如果要声明超过一个以上的数据,各个操作数之间必须以逗号隔开。
db 0,0,0,0,0 声明的数据可以是任意的大小,只要不超过伪指令所指示的大小。 这里我们吧他们的值都初始化为0。
除此之外,dw用于声明字数据,DD(Declare Double Word) 用于声明双子(两个字)数据 DQ(Double Quad Word)用于声明四个数据
DD DW DQ DB不是机器指令,只是编译器提供的汇编指令,所以称为伪指令.编译成功之后,为指令就消失了,所以程序执行时,伪指令是得不到处理器光顾的
实际上,程序执行时,伪指令已不存在。
18.div
第一种类型是用16位的二进制数除以8位的二进制数,在这种情况下,被除数必须在寄存器ax中,必须实现先传送到ax寄存器里面,除数可以是8位的通用寄存器
或者内存单元提供,指令执行后,商在寄存器al中, 余数子啊寄存器ah中
第二种类型是用32位的二进制数除以16位的二进制数,在这种情况下,因为16位的处理器无法直接提供32位的被除数,故要求被除数的高16位在dx中,低16位
在ax中,除数可以由16位的通用寄存器或者内存单元提供,执行指令之后,商在ax中,余数在dx中。
div cx
div wird[0x230]
严格的来说,div应该叫做无符号除法指令,因为这条指令只能工作于无符号数,欢聚话说,只能从无符号数的角度来解释他的执行结果才能说得通。
19.段超越前缀
div byte [cs:0x0023]
div byte [es:0x0023]
任何时候,只要是在指令中涉及内存地址的,都允许使用段超越前缀。
在一个源程序中,通常不可能知道汇编地址的具体数值,只能使用标号,所以,指令中的地址部分更常见的形式是使用标号。
dividnd dw 0x3f0
divisor db 0x3f
…
mov ax,[dividnd]
mov byte [divisor]
21.主引导扇区
处理器加电或者复位之后,如果硬盘是首选的启动设备,rom-bios将试图读取硬盘的0面0到1扇区,传统上,这就是主引导扇区,读取的主引导扇区数据有512字节,rom-bios程序将它加载到逻辑地址0x0000:0x7c00
处,也就是物理地址0x07c00处,然后判断它是否有效。一个有效的主引导扇区,其最后两个字节应当是0x55和0xaa,如果主引导扇区是有效的,则以一个段间转移指令jmp 0x0000:0x7c00跳到那里继续执行。
mov [0x7c00+number+0x00],dl
所以在在编写主引导扇区程序时,我们就要考虑到这一点
指令中的目的操作数是在编译节点确定的,因此,在编译阶段,编译器同样会首先将他转换成一下的形式,在进一步生产机器码
mov [0x7d2e],dl
22.xor 在数字逻辑里是异或的意思,或者叫互斥或,互斥的或运算。
xor指令的目的操作数可以是通用寄存器和内存单元,也就是左边的操作数,源操作数可以是通用寄存器,内存单元和立即数,注意:不允许两个操作数同时为内存单元),而且
异或操作是在两个操作数相对应的比特之间单独运行的。一般来说,xor指令的两个操作数相当具有相同的数据宽度。
xor的两个操作数,1 xor 1 = 0 0 xor 0 = 0 其他的是1
xor dx,dx 直接吧dx中的目的操作数和源操作数清零 比 mov dx,0 少一个字节的机器码,而且更快
23.add
add 指令需要两个操作数,目的操作数可以是8位或者16位的通用寄存器,或者指向8位或者16位实际操作数的内存地址,
源操作数可以是相同数据宽度的8位或者16位通用寄存器,指向8位或者16位实际操作数的内存地址,或者立即数
注意:不允许两个操作数同时为内存单元,相加后,结果保存在目的操作数中。
32位: add dword [ecx],[0x000005f]
24.对于处理器来说,取指令,执行是永无止境的。
25.无限循环
infi:jmp near infi
jmp 是转移指令,用于使处理器脱离当前的执行序列,转移到指定的地方执行,关键字near表示表示目标位置
依然在当前代码段内,上面这段这条指令唯一特殊的地方在于他不是转移带别处,而是转移到自己。也就是说
他将会不停的重复执行自己。
注意:jmp near 0x7c00+infi 这样写是错误的。
jmp 格式
觉对地址:
jmp 0x5000:0xf0c0
将指令中给出的段地址传送到段寄存器cs;将偏移地址传送到指令指针寄存器ip,从而转移到目标位置处接着执行。
相对进转移
jmp near infi 实际上是一个3字节指令,操作码是0xe9,后跟一个16位(两字节)的操作数。
该操作数并非目标位置的偏移地址,而是 目标位置相对与当当前指令处的偏移量(以字节为单位)。
编译器的做法:用标号(目标地址)出的汇编地址减去当前指令的汇编地址,再减去当前指令的长度(3),就
得到jmp near infi指令的实际操作数。
near 仅仅用于指示相对量是16位的。
在指令执行的阶段,处理器用指令寄存器ip的near加上该指令的操作数,在加上指令的长度(3)。就得到实际偏移
地址,同时cs寄存器的内容不变,因为改变了ip的内容,这直接导致处理器的指令执行流程转向了目标位置。
主引导扇区在系统启动过程中并非是唯一的选择,如果主引导扇区不可用,系统还有其他选择,比如可以从光盘和u盘启动。
一个有效的主引导扇区,最后两个字节的数据必须是0x55,和0xaa。否则,这个扇区里保存的就不是一些有意而为的数据。
dw 0xaa55
在intel处理器上,将一个字写入内存时,是采用低端字节序列的,低字节0x55置入底地址(在前),高字节0xaa在高地址端(在后)。
29.伪指令times 可用于重复它后面的指令若干次。
times 20 mov ax,bx
将在编译时重复生产mov ax,bx指令20次,即重复该指令的机器码(89 d8) 20次。
times 203 db 0 将在编译时保留203个为0的字节。
在nasm里面,"" 是换行符,当一行写不下时,可以在行尾使用这个符号,以表示下一行与当前行应该合并为一行。
如果程序加载时,不是从段内偏移地址为0的地方开始的,而是0x7c00,那么lable_a的实际偏移地址就是0x7c05.
32.尽管Bios将主引导扇区加载到物理地址0x7c00处,但我们却可以认为它是从0x07c0:0x0000处开始加载的。
movsb movsw
这两个指令通常用于把数据从内存中的一个地方批量地传动(复制)到另外一个地方,处理器把他们看成数据串。
但是movsb的传送是以字节为单位的,movsw的传送是以字为单位的。
+(1).movsb和movsw指令执行时,原始数据串的段地址由ds制定,偏移地址由si指定,简写为ds:si;
要传送的目的地址由es:di指定。传送的字节数(movsb) 或者字数(movsw) 由CX指定,除此之外,还要
指定是正向还是反向传送,正向传送是之指传送操作的方向是从内存区域的低地址到高地址。反向传送则相反。
正向传送时,每传送一个字节或者一个字,SI,DI加1或者加2,反向传送时,每传送一个字节或者一个字时,SI和
DI减去2,每次传送CS的内容自动减1
-
第6位
zf(zero flag),或非们的输出送出一个触发器,这就是标志寄存器zf位,也就是说,如果计算结果为0,这一位被
置为1,如果计算结果为零,是"真"的,否则清除此位(0). -
第10位是方向标志df(direction flag) 通过将一位清零或者置1,就能控制movsb 和movsw 的传送方向。
cld 这是个无操作数指令,相反的是std;cld将df表示清零,指示传送的是正方向,std,他将df标志位(1),此时,
传送的方向从高地址到低地址。
rep,意思是cx不为零则重复,rep movsw ,它将重复执行movsw直到CX的内容为零。
38.loop
loop 指令的功能是重复执行一段相同的代码,处理器在执行他的时候会顺序做两件事。
(1).将寄存器cx的内容减一
(2).如果cx的内容不为零,转移到指定的位置处去执行,否则顺序执行后面的指令。
loop 后面跟着一个字节的操作数,而且也是相对与标号处的偏移量,在编译阶段,编译器用dight所在的汇编地址
减去loop指令的汇编地址。再减去loop的长度(2)来得到的,loop(本身的操作码本身是一个字节的)。
如果是在32位环境下:
那么,循环的次数存储在ecx中。
39.
在8086处理器上,如果要用寄存器来提供偏移地址,只能使用bx,si,di,bp,不能使用其他寄存器。
bx 最初的功能之一就是用来提供数据访问的基地址,所以又叫基址寄存器(base address register)
si 是源索引寄存器(source index) , di是目标索引寄存器(destination index) 用于数据传送操作。
注意:可以在任何带有内存操作数的指令中使用bx,si或者di提供偏移地址。
40.inc 自加
add bx,1 一样,但是前者的机器码更短,速度更快。
格式:
inc al
inc byte [bx]
inc word [label_a]
dec ,与inc指令对应,用于将目标操作数的内容减一。他们的指令格式相同。
在指令的地址部分使用寄存器,而不是数值或者标号,有一个明显的好处,那就是可以在循环体很方便的改变偏移地址。
- neg
neg 指令带有一个操作数.可以是8位或者16位的寄存器,或者是内存单元.
neg al
neg dx
neg word [lable_a] ; 注意这里用word修饰了
功能:用0减去指令中的操作数,如果al中的内容是00001000(十进制数8),执行neg al后,al中的内容为(11111000)十进制数-8
44.sub
sub与add类似,目标的操作数可以是8位或者16位通用寄存器,也可以是内存单元,源操作数可以是通用寄存器,也可以是内存单元或者立即数。
(不允许两个操作数同时为内存单元)
例子
sub ah,al
sub dx,ax
sub [lable_a],ch
45
因为处理器没有减法,所以,sub ah,al指令实际上等效于下面的指令:
neg al
add ah,al
46
idiv
idiv的指令格式和div相同,除了他是专门用来计算有符号数的,如果要计算无符号数,就必须用div
mov ax,0xf0c0
mov bl,0x10
idiv bl ; 除法的结果是-244,但是al表示不了这么大的数。
xor dx,dx
mov ax,0xf0c0
cwd ;扩展到dx -3904的16位的二进制形式和32位的二进制形式是不一样的。
mov bx,0x10
idiv bl
47
8086处理器之允许以下几种基址寄存器和变址寄存器的组合
[bx+si]
[bx+di]
[bp+si]
[bp+di] ;这些组合可以用于任何带有内存操作数的指令中。
基偶标志位PF
当运算结果出来之后,如果最低8位中,有偶数个为1的比特,则PF为1,否则为0
进位标志CF
当处理器进行算数操作时,如果最高位有向前进或借位的情况发生,CF=1,否则CF=0
但是有的指令除外,比如inc 和dec
溢出标志OF
也就是说这个标志需要先看有符号的运算还是无符号的运算。
在有符号运算中,溢出就意味着一个错误的计算结果。所以我们可以看这个标志是否置位来判断是不是错误了。
也就是说这个标志所做的事情是:假定进行的是有符号数的计算,并根据结果提供of标志,至于如何处理,是你自己的事情。
如果你进行的是无符号数运算,那么就不用理会这个标志。
任何时候,处理器都不可能知道你的意图,不知道你进行的是有符号数运算,还是无符号数的运算。
光标现实标志的名称,怎么知道某个标志位是0还是1呢?如果显示的标志名称是小写的,那么,说明该标志为0,否则,该标志为1。
nasm编译器提供了一个标记$,该标记等同与标号,你可以把他看作是一个隐藏在当前行首的标号。因此,jmp near $的意思是,转移到当前指令继续执行,
他和infi:jmp near infi是一样的,没有区别,但不需要使用标号,更不必给标号起一个有意义的名字而伤脑筋。
$$是nasm编译器提供的另一个标记,代表当前汇编节段的起始汇编地址,当前程序如果没有定义节或段,就默认地定义一个汇编段,而且起始的汇编地址是0(程序起始处)。
这样,用当前汇编地址减去程序开头的汇编地址(0),就是程序的实体大小。
or指令的目的操作数可以是8位或者16位的通用寄存器,或者包含8/16位实际操作数的内存单元,源操作数可以是与目的操作数数据宽度相同的通用寄存器,内存单元或者立即数。
注意:or指令不允许目的操作数和源操作数都是内存单元的情况。
or与and指令一样,执行之后,cf和of被清0,其他情况依状态而定。
push
在16位的处理器上,push指令的操作数可以是16位的寄存器或者内存单元
push ax
push word [lable_a]
8086的处理器只能压入一个字,但是气候的32位和64位处理器允许压入字。双字,或者四字,所以关键字是必不可少的。
处理器在执行push指令时,首先将栈指针寄存器sp的内容减去操作数的字长(以字节为单位的长度,在16位处理器上是2),然后,吧要压入栈的数据存放到逻辑
地址ss:sp所指向的内存位置(和其他段的读写一样,把栈段寄存器ss的内容左移4字节,加上栈指针寄存器sp提供的偏移地址)
代码在处理器上执行的时候,是由低地址到高地址执行,而压栈操作则正好相反。
标志位:push不影响任何标志位
push 是先减去用sp减去操作数的字长,在写入操作数
push imm8 ; 操作码位6a
push imm16 ; 操作码为68
push imm32 ; 操作码位68
注意:无论在什么时候,处理器都不会真的压入一字节,要么压入字,要么压入双字。
push byte 0x55
16 位:处理器执行的时候,将该字节的符号位扩展到高8位,将sp的内容减2
32 位:处理器执行的时候,将该字节操作数位扩展到高24位,即0x00000055, 将esp的内容减少4
push word 0xfffb
16 位:直接压入该字。
32位 :压入的内容是该符号位扩展到高16位,即0xfffffffb.将esp的内容减4
push dword 0xfb
16 位 和 32 位模式下,压入的都是0x000000fb.而且sp 或者esp 都减去4
对于实际操作数位于通用寄存器,或者位于内存单元的情况,只能压入字或者双字,指令格式:
push r/m16
push r/m32
无论被压入的数位于寄存器,还是位于内存单元,在16位模式下,如果压入的是字的操作数,那么sp的内容双字。应当先将sp的内容减去4
32位的情况是一样的。
push cs
push ds
push es
push fs
push gs
push ss
在16位模式下,将sp的内容减去2,然后直接压入段寄存器。
在32位模式下,要先将段寄存器的内容用0扩展到32位。即高16位全位0,然后将esp的内容减去4,在压入32位值。
pop
是先弹出操作数,在加上操作数的字长。
print-stack 命令可以查看栈。
你应当在执行rep movsb,repmovsw和loop指令的时候,使用调试命令n,bochs将自动完成循环过程,并在循环体外的下一条指令前停住。
57.寻址方式:数从那里来,处理后送到那里去,简单的来说就是,寻址方式就是如何知道要操作的数据,以及如何处理知道存放操作结果的地方。
寄存器寻址
指令执行时,操作的数位于寄存器中,可以从寄存器中取得。
立即寻址
立即寻址又叫立即数寻址,也就是说,指令的操作数是一个立即数。
mov dx,label_a 目的操作数也采用寄存器寻址方式,尽管源操作数是一个标号,但是,标号是数值的等价形式,代表了它所在位置的汇编地址,因此,
在编译阶段,他会被转换为一个立即数,因此,该指令的源操作数也采用了立即寻址方式。
立即寻址的操作数位于指令中,是指令的一部分
直接寻址
使用该寻址方式的操作数是一个偏移地址,而且给出了该便宜地址的具体数值。
mov ax,[0x5c0f]
xor byte [es:lable_b]:0x05
基址寻址:就是在指令的地址部分使用基址寄存器bx或者bp来提供便宜地址。
mov [bx],dx
add byte [bx],0x55
基址寻址的元寄存器有可以是bp,但是在形成20位的物理地址时,默认的段寄存器是ss,也就是说,他经常用于访问栈。
mov ax,[bp]
这条指令执行时,处理器将栈段寄存器ss的内容左移4位,加上寄存器bp的内容,形成20位的物理地址,并
将该地址出的一个字传送到寄存器ax中。
基址寻址允许在基址寄存器的基础上使用一个便宜量
mov dx,[bp-2] 这样一来,不改变bp,就可以访问其他的元素.
变址寻址
变址寻址类似与基址寻址,唯一不同之处在与这种寻址方式使用的是变址寄存器(或者是索引寄存器)
si和di.
除非使用了段超越前缀,处理器会访问由段寄存器ds指向的数据段,便宜地址由寄存器si或者di提供。
变址寻址方式也允许带一个偏移量。
mov [si+0x100],al
and byte [di+lable_a],0x80
基址变址寻址
使用基址变址的操作数可以使用一个基址寄存器(bx,bp),外加一个变址寄存器(si或di)
他的基本形式:
mov ax,[bx+si]
add word [bx+di],0x3000
当处理器执行这条指令的时候,把数据段寄存器ds的内容左移四位,加上基址寄存器bx的内容,再加上变址寄存器si的内容,共同形成20位的物理地址。
注意:基址变址寻址允许在基址寄存器和变址寄存器的基础上带一个偏移量。
比如:
mov [bx+si+0x100],al
and byte [bx+di+label_a],0x80
用户程序头部需要源程序以一个段的形式出现,
SECTION header vstart=0
而且,因为他是头部,所以,该段当然必须是第一个被定义的段,且总是位于整个程序的开头.
用户程序头部起码要包含以下信息:
1.用户程序的尺寸,即以字节为单位的大小。加载器需要根据这一信息来决定读取多少个逻辑区。
由于该段并没有vstat字句,所以,标号"program_end" 所代表的汇编地址是从整个程序的开头开始计算的。
2.应用程序的入口点,包括段地址和偏移地址。必须在头部给出第一条指令的段地址和便宜地址,这就是所谓的应用程序入口点.
61.双字在内存中的存放也是按低端序的,低字节保存到低地址,高字保存在高地址,同时,每个字又按低段字节序,低字节在低地址,高字节在高地址。
62.段的汇编地址其实就是段内的第一个元素(数据,指令)的汇编地址,也就是说这个汇编地址
63.equ
常量是用伪指令equ声明的,它的意思是"等于",本语句的意思是,用标号app_lba_start来代表数值100,今后,当我们要用100的时候,
mov al,100
可以这样写 mov al,app_lba_start ,注意equ声明的数值不在用任何汇编地址。也不再允许时占用任何内存位置。他就仅仅代表一个数值。
i/o接口可以是一个电路板,也可能是一块小芯片,这取决于他有多复杂,无论如何,他是一个典型的变换器,或者说是一个翻译器,在一边,他按处理器的信号规则工作,
负责把处理器的信号转换成外围设备能接受的另一种信号,在另外一边,他也做着同样的工作,吧外围设备的信号变换成处理器可以接受的形式。
但处理器想某个设备说话时,ICH会接到通知,然后,他负责提供相应的传输通道和其他的辅助支持,并命令所有其他无关设备闭嘴,同样,当某个设备要跟处理器说话,
情况也是一样。
引脚M/IO#,#表示低电平有效。也就是说,当处理器访问内存的时,它会让M/IO#引脚呈高电平,相反,如果处理器访问I/O端口,那么M/IO#引脚呈低平,内存电路被禁止。
与此同时,处理器发出的地址和M/IO#信号一起用于打个某个I/O接口,如果该I/O接口分配的端口号与处理器地址相吻合的话。
在intel的系统中,只允许65536(十进制数)个端口存在,端口号从0到65535(0x0000~0xffff)。
因为是独立编址,所以,端口的访问不能使用类似与mov这样的指令,取而代之的是in和out指令
in
in指令是从端口读,一般形式:
in al,dx 当访问8位的端口的时候
in ax,dx 当访问16位的端口的时候
也就是说,in指令的目的操作数必须是寄存器al或者是ax 指令的源操作数必须是dx
上面的指令码都是一个字节
下面的操作码都是两个字节,并且不影响任何标志位
in al,0xf0
in ax,0x03 因为是两个字节的,所以后面只能用一个字节的数,也就是0~255
也就是说右边的内容是端口号。
70.
out 通过端口想外围设备发送数据,必须通过out指令
out指令正好和in 指令相反,目的操作数可以是8位立即数或者dx,源操作数必须是寄存器al或者ax
例子:
out 0x37,al
out 0xf5,ax
out dx,al
out dx,ax
硬盘读写的基本单位是扇区,就是说,要读就至少读一个扇区,要写就至少写一个扇区,不可能
仅读写一个扇中的几个字节,这样一来,就是的主机和硬盘之间的数据交换是成块的,所以硬盘是
典型的块设备。
个人计算机上的主硬盘控制器被分配了8位端口,端口号从0x1f0到0x1f7
1.
设置要读取的扇区数量,这个数值要写入0x1f2段口。这是个8位端口,因此每次只能读写255个扇区
mov dx,0x1f2 ;
mov al,0x01 ; 读取一个扇区
out dx,al
注意:如果写入的值是0,则表示要读取的256个扇区,每读一个扇区,这个数值就减一,因此,如果在读写的过程中
发生错误,该端口包含着未读取的扇区数。
2.设置lba扇区号
因为扇区的读写是连续的,因此只需要给出第一个扇区的编号就可以了。
mov dx,0x1f3
mov al,0x02
out dx,al ; lab地址7~0
inc dx ; 0x1f4
mov al,0x00
out dx,al ; lab地址15~8
inc dx ; 0x1f5
out dx,al ; lba 地址23~16
inc dx ;0x1f6
mov al,0xe0 ; lba模式,主硬盘,以及lba地址27~24 高位4个位置,7~5 111 代表lba模式,所以是e,低4位就是逻辑扇区号27~24号
out dx,al
读命令
mov dx,0x1f7
mov al,0x20 ;读命令 0x20是读命令
out dx,al
等待读写操作完成,端口0x1f7既是命令端口,又是状态端口。
mov dx,0x1f7
.waits
in al,dx
and al,0x88 10001000 ;这里就是想保留地7位和第3位
cmp al,0x08 ;然后看低3位是否为1和低7位是否为0
jnz .waits ;不忙,且硬盘已准备号数据传输
注意:第7位如果为1的化,就代表硬盘忙,如果硬盘不为1的化,就代表硬盘不繁忙,第3位置为1的化就代表
硬盘已经准备好和主机交换数据了
5
连续取出数据
0x1f0是硬盘接口的数据端口,而且还是一个16位端口。一旦硬盘控制器空闲,且准备就绪,就可以连续从这个端口
写入或者读取数据
mov cx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax ; 这里读入了两个字节
add bx,2
loop .readw
0x1f1端口是错误寄存器,包含硬盘驱动器最后一次执行命令后的状态(错误原因)
在调用过程中,程序会用到一些寄存器,在过程返回之前,可能还要继续使用,为了不失连续性,在过程的开头,应当将本过程
要用到(内容肯定会被破坏)的寄存器临时压栈,并在返回到调用点之前出栈恢复
mov al,0xe0 ;11100000
or al,ah ;也就是说要保留后面的lba地址27~24
out dx,al
8086处理器支持四种调用方式
第一种是16位相对近调用,近调用的意思是被调用的目标过程位于当前代码段,而非不同的代码段,所以只需要提供偏移地址就可以。
因为是相对调用,所以该操作数是当前call指令相对目标过程的偏移量
计算过程,用目标过程的汇编地址减去当前call指令的汇编地址,再减去call指令以字节为单位的长度(3),保留16位的结果。
所以,它的机器指令操作数是一个16位的有符号数,也就是说,被调用过程的首地址位于当前call指令-32768~32767字节的地方。
执行阶段:它用指令指针寄存器ip的当前内容加上指令中的操作数,在加上3,得到一个新的偏移地址。
重点:将ip的原有内容压入栈,最后用刚才计算出的偏移地址取代ip原有的内容。
第二种是16位间接绝对近调用
call cx
call [0x3000]
call [bx]
call [bx+si+0x02]
; 要先访问内存才能取得目标偏移地址,取得一个字,然后直接取代指令指针寄存器ip原有的内容
由于间接绝对近调用指令操作数是16位的绝对地址,因此,他可以调用当前代码段任何位置处的过程
绝对调用的意思是,这里采用的是真实的地址,不是相对地址。
第三中是16位直接绝对远调用
16位是针对偏移地址来说的,而不是限定段地址。
直接的意思是,段地址和偏移地址直接在call指令中给出的。
这里的地址是绝对地址
call 0x2000:0x0030 偏移地址在前,段地址在后
执行的时候,首先将代码段寄存器cs的内容压入栈,在ip的内容压入栈,然后替换。
第四种是16位间接绝对远调用
这也属于段间调用,被调用过程位于另一个代码段内,而且,被调用过程所在的段地址和偏移地址是间接给出的
16位 同样是用来限定偏移地址的。
例子:
call far [0x2000]
call far [proc_1]
call far [bx]
call far [bx+si]
注意:far 必须要加上,从偏移地址中取出两个字。
ret 和 retf经常做call 和call far 的配对指令。
ret 是近返回指令,当他执行时,处理器只做一件事,那就是从栈中弹出一个字到指令指针寄存器ip中。
retf是远返回指令,当他执行时,处理器分别从栈中弹出两个字到指令指针ip和cs中。
77.尽管call指令通常需要ret/retf和他配对,但ret/retf指令却并不以来call指令.
adc 带进位加法,他将目的操作数和源操作数想加,然后加上标志寄存器cf位的值(0或者1)
shr
shr 指令的目的操作数可以是8位或16位的通用寄存器或者内存单元,源操作数可以是数字1,8位立即数,或者寄存器cl.
源操作数为1的逻辑右移指令是特殊的优化指令,比如shr ax,1 他的机器码是D1 E8,而类似的指令shr ax,5 则拥有完全不同的机器码c1 e8 05
如果shr指令的源操作数是寄存器,则只能使用cl。
在80286之后的ia-32处理器在执行本命令时,会先将源操作数的高3位清零,也就是说,最大的移位次数是31
shr的配对指令是逻辑左移shl(shitf logical letf),他的指令格式和shr相同,只不过他是向左移动。
shl dword [eax*2+0x08],cl ;先将cl的内容同0x1f做逻辑与,也就是说,仅保留源操作数的低5位,所以,实际移动的最大的次数最大为31
ror 循环右移(rotate rigth) :循环右移指令执行时,每右移一次,移出的比特既送到标志寄存器cf位,也送进左边空出的位。
ror 的配对指令是循环左移指令rol(rotate left),ror rol shl shr 指令的格式是相同的
resb的意识是从当前位置开始,保留指定数量的字节,但不初始化他们的值,在源程序编译时,编译器会保留一段内存区域,用来存放编译后的内容。
当他看到这条指令时,他仅仅是跳过指定数量的字节数,而不管里面的原始内容是什么,也就是说,跳过这段空间,每个字节的值是不确定的。
resb 256
resw 100 ;声明100个未初始化的字
resd 50 ; 声明50个未初始化的字
需要回车换行的地方按照老传统插入这两个代码。
83.在所有要显示内容的最后,是数值0,用来标志字符串的结束,这样的字符串为0终止的字符串,在高级语言里经常使用。
84.一个数与自己做或运算,结果还是自己。
85.0表示光标在屏幕上第0行第0列,80表示他在第一行第0列,因为标准vga文本模式是25行,每行80个字符,也就是说,当光标在屏幕右下
角时,该值为25*80-1 = 1999
86.光标寄存器是可读可写的,你可以从光标中读出光标的位置,也可以通过他设置光标的位置,能够通过写入一个数值来设定光标的位置,这是我们
的责任,因为显卡从来不自动移动光标的位置,所以移动光标的位置是我们的责任。
87.0x0d 指的是回车,\r是把光标置于本行航首
0x0a 值的是换行,\n 是把光标置于下一行的同一列
显卡寄存器
端口号:0x3d4 可以用他写入一个值,用来指定内部的某个寄存器。 比如两个光标寄存器,其索引值为14(0x0e) 和15 (0x0f)分别用来提供光标位置的高
8位和第8位
mov dx,0x3d4
mov al,0x0e
out dx,al 通过索引寄存器告诉显卡,现在要操作0x0e号寄存器。
mov dx,0x3d5
in al,dx
mov ah,al ; 通过数据端口从0x0e号端口读出1字节数据,并且传送到寄存器ah中,这个是屏幕光标位置的高8位
端口号:0x3d5 数据端口,我们可以通过这个端口号进行读写。
mov dx,0x3d5
in al,dx
mul 是乘法指令
mul r/m8 ; ax = alr/m8
mul r/m16 ; dx:ax = axr/m16
mul r/m32 ; EDX:EAX <- EAX*r/m32
r表示通用寄存器,m表示内存单元,mul指令可以用8位的通用寄存器或者内存单元中的数和寄存器al中的内容相乘,结果是16位,在ax寄存器中。
也可以用16位的通用寄存器或者内存单元中的数和寄存器ax中的内容相乘,结果是32位,高16位和低16位在dx和ax中。
mul 指令执行后,要是结果的高一半全为0,则of和of清0,否则置1
有符号乘法指令imul 与此相同.
90.push 指令不能压入立即数
91.硬盘的工作速度比处理器至少慢几千万甚至几亿倍,像打印机这类设备更不用说,在等待的时候,处理器唯一能做的,就是不停的观察外部设备的状态变化。
当一个程序正在等待输入输出时,允许另一个程序从处理器那里取得执行权。
当一个程序执行时,它是不会知道还有别的程序正在眼巴巴地等着执行,在这种情况下,中断这种工作机制就应运而生了。
92.中断就是打断处理器当前的执行流程,去执行另外一些和当前工作不想干的指令,执行完之后,还可以返回到原来的程序流程继续执行。
93.所有的严重事件都必须无条件地加以处理,这种类型的中断是不会被阻断和屏蔽的,称为非屏蔽中断(Non Maskable Interrupt , NMI);
94.NMI中断信号由0跳到1后,至少要维持4个以上的时钟周期才算有效,才嫩被识别。
95.当一个中断发生时,处理器将会通过中断引脚NMI和INTR得到通知
应当知道:
发生了什么事,以便采取适当的处理措施,每种类型的中断都被统一编号,这成为中断类型号,中断向量或者中断号。
96.在处理器内部,标志寄存器有一个标志位IF,这就是中断标志,当IF为0时,所有从处理器INTR引脚来的中断信号都被屏蔽掉,当其为1时,
处理器可以接受和相应中断。
IF标志位可以通过两个指令sli和sti来改变,这两条指令都没有操作数,cli(clear interrpt flag) 用于清除IF标志位,sti(set interrupt flag) 用于置位IF标志.
97.有多个中断同时发生时,8259芯片会记住他们,并按照一定的策略觉得为谁服务
总体来说,中断的优先级和引脚是相关的,主片的ir0引脚优先级最高,ir7引脚最低。
当一个中断事件正在处理时,如果来了一个优先级更高的中断事件,允许暂停中断当前的中断处理,先为优先级较高的中断时间服务,这称为中断嵌套。
98.当处理器执行任何一条改变栈段寄存器ss的指令时,他会在下一条指令执行完期间禁止中断。处理器在设计的时候就规定,当遇到修改段寄存器ss的指令时,在这条指令和下一条指令执行
完毕期间,禁止中断,以此来保护栈,换句话说,你应该在修改段寄存器ss的指令时候,经跟着一条修改栈指针sp
例子:
mov ss,ax
mov sp,ss_pointer
这两行代码执行期间,是禁止中断的。
99.为什么后面没有写入现实属性的字节,原因很简单,在写入其他内容之前,显存里全是黑底白字的空白字符。所以不需要重写黑底白字的属性。
100.在很早的时候开始,端口0x70的最高位(bit 7) 是控制NMI中断的开关,当它为0时,允许NMI中断到达处理器,
为1时,则阻断所有的NMI信号,其他7个比特,即0~6位,实际上用于指定CMOS RAM单元的索引号,这种规定知道现在
都没有改变。
注意:NMI 中断不能被if标志屏蔽
101.not
not 指令将字符的现实属性反转,not是按位取反指令,其格式为
not r/m8
not r/m16
not 指令执行时,会将操作数的每一位反转,原来的0变成1,原来的1变成0。
mov al,0x1f
not al ; 执行后,al的内容为0xe0
102.hlt指令使处理器停止执行指令,并处于停机状态,这将降低处理器的功耗。处于停机状态的处理器
可以被外部中断唤醒并恢复执行,而且会继续执行hlt后面的指令。
相对于jmp $指令,使用hlt指令会大大降低处理器的占用率。
103.test指令在功能上和and指令是一样的,都是将两个操作数按照未进行逻辑“与”,并根据结果设置相应的
标志位,但是test指令执行后,运算结果被抛弃(不改变或破坏两个操作数的内容)。
指令格式:
test r/m8,imm8
test r/m16,imm16
test r/m8,r8
test r/m16,r16
和and指令一样,test指令执行后,OF = CF = 0,对zf,sf,pf的影响视测试结果而定。
对af的影响未定义。
iret 回到中断之前的地方继续执行,iret的意思是interrupy return
105.和硬件中断不同,内部中断发生在处理器内部,是由执行的指令引起的,比如当处理器检测到div或者idiv指令的除数
为零时,或者出发的结果溢出时,将产生中断0(0号中断)。
内部中断不受标志寄存器if位的影响,也不需要中断识别总线周期,他的中断类型是固定的,可以立即转入
相应的处理过程。
div r/m32
idiv r/m32
在这里,被除数是64位的,高32位在edx寄存器,低32位在eax寄存器,除数是32位的,位于32位的寄存器,或者存放
有32位实际操作数的内存地址,执行指令后,32位的商在eax寄存器,32位的余数在edx寄存器中。
106.32位寄存器的高16位是不可独立使用的,但地16位保持同16位处理器的兼容性。因此,在任何时候他们都可以照以往一样使用
mov ah,0x02
mov al,0x03
add ax,si
107.32为处理器扩展了ip,使之达到32位,即eip,当他在16位模式下时,依然使用16位的ip
108.IA-32架构的处理器是基于分段模型的,因此,32位处理器依然需要以段为单元访问内存,即使他工作在32位模式下,不过他也提供了一种
变通的方案,即,只分一个段,段的基地址是0x000000,段的长度(大小)是4gb,在这种情况下,可以视为不分段,即平坦模式。
109.传统的段寄存器,如cs,ss,ds,es,保存的不再是16位段基地址,而是段的选择子,用于选择多要访问的段,因此,严格的说,他的新名字叫做段
选择器,除了段选择器之外,每个段寄存器还包括一个不可见的部分,称我描述符告诉缓存器,里面有段的而基地址和各种访问属性。
110.告诉缓存是处理器与内存(dram)之间的一个静态存储器,容量较小,但速度可以与处理器匹配。
111.每当处理器要访问内存时,首先检索告诉缓存,如果要访问的内容已经在告诉缓存中,可以用极快的速度直接从高速缓存中取得,这成为命中(hit)
否则,这成为不中(miss),在不中的情况下,处理器在取得需要的内容之前必须重新装在告诉缓存,而不只是直接到内容中去取得那个内容,告诉缓存
的装在是以块为单位的,包括那个所需数据的龄近内存。为此,需要额外的时间来等待块从内存载入告诉缓存,在该过程中所损失的时间成为不中惩罚。
112.在处理器内部,有大量的临时寄存器,处理器可以重命名这些寄存器以代表一个逻辑单元,比如eax
寄存器重命名以一种完全自动和非常简单的方式工作,每当指令写逻辑寄存器时,处理器就为那个逻辑寄存器
分配一个新的临时寄存器。
所有通用寄存器,栈指针,标志,浮点寄存器,甚至段寄存器都有可能被重命名。
113.当处理器运行在16位模式时,如果没有指令前缀0x66,则认为指令是传统的16位寻址方式,若有指令前缀
0x66,则指令是最新的32位寻址方式,如果处理器当前运行在32位模式下,且没有指令前缀0x66,则默认为32
位寻址方式,否则就是传统的16位寻址方式。
114.如果指令中使用了立即数,那么,该数值默认是32位。
mov ecx,0x55 ; ECX = 0x00000055
115.如果指令中的操作数是指向内存单元的地址,那么,该地址默认是32位的段内偏移地址,或者叫段内偏移量。
mov edx,[mem] ; mem 是一个32位的段内偏移地址。
32位的有效地址
有效地址可以使用全部的32位通用寄存器作为基址寄存器,同时,还可以加上一个除esp之外的32位通用寄存器
作为变址寄存器,变址寄存器还允许乘以1,2,4或者8作为比例因子。最后,还允许加上一个8位或者32位
的偏移量。
注意:
mov ax,[sp] ; 这个在16位是不可以的
mov eax,[esp] ; 这个在32位地址上是可以的
117.在编写程序时,就应当考虑到指令的运行环境,为了指明程序的默认运行环境,编译器提供了伪指令bits,
用于指明其后的指令应该被编译成16位的,还是32位的。
比如:
bits 16
mov cx,dx ; 89 d1
mov eax,ebx; 66 89 d8
bits 32
mov cx,dx ; 66 89 d1
mob eax,ebx ; 89 db
bits 的格式
[bits 32]
mov ecx,edx
bits 16
mov ax,bx
也即使说bits 16 或者bits 32 可以放在方括号中,也可以没有方括号。
16位模式是默认的编译模式,如果没有指定指令的编译模式,则默认是 bits 16 的。
在保护模式下,对内存的访问任然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行等级。
登记的信息包括:段的起始地址,段的界限等各种访问熟悉,当你访问的偏移地址超出段的界限时,处理器就会阻止这种访问,并产生一个叫做内部
异常的中断。
119.段描述符,和一个段相关的信息需要8字节来描述。为了存放这些描述符,需要在内存中开辟出一段空间,在这段空间里,
所有的描述符都是挨在一起的,集中存放的,这就构成一个描述符表。
全局描述符表,全局,意味着该表是为整个软硬件系统服务的。在进入保护模式之前,必须要定义全局描述符表。
gdt_base dd 0
120.全局描述符表寄存器(GDTR)
两个部分:分别是32位的线性地址,和16位的边界,32位的处理器具有32根地址线,一共能访问4GB内存。所以32位线性地址部分保存的是全局描述符
表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数-1);换句话说,全局描述符的界限
值就是表内最后1字节的偏移量。第一个字节的偏移量是0。最后一个字节的偏移量是表大小-1;
47~16位是全局描述符寄存器,15~0位是全局描述符表边界。
121.在保护模式下,内存的访问机制完全不同,必须通过描述符来进行,所以,这些段必须在GDT中定义。
122.段界限用来限制段的扩展范围,因为访问的方法是用段基地址加上偏移量,所以,对于向上扩展的段,如代码段和数据段,偏移量是从0开始的。
段界限决定了偏移量的最大值,对于向下扩展的段,如栈段,段界限决定了偏移量的最小值。
123.R属性并非用来限制处理器,而是用来限制程序的指令和行为,一个典型的例子是使用段超越前缀cs:来访问代码段中的内容。
124.虚拟内存管理的实现:数据段和代码段,的A位是已访问位,用于只是他所指示他所指向的段最近是否被访问过,当描述符创建的时候,应该清0。
之后,每当该段被访问时,处理器自动将该位置1,对该位清0是由(操作系统)负责的,通过定期见识该位的状态,就可以统计出该段的使用频率,
当内存空间紧张的时候,可以吧不经常使用的段退避到硬盘上,从而实现虚拟内存管理。
125.处理器规定,gdt中的第一个描述符必须是空描述符,或者叫哑描述符或者null描述符。很多时候,寄存器和内存段元的初始值会为0,再加上程序
设计有问题,就会在无意中用全0的索引来选择描述符,因此,处理器要求将第一个描述符定义成空描述符。
;创建0#描述符,它是空描述符。这是处理器的要求。
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00
126.假设段界限的值是0x07a00,这是一个栈段,那么0x07a00+1 就是esp寄存器所允许的最小值,当执行push,call这样的隐式栈操作时,处理器会检查
esp寄存器的值,一旦发现他小于等于这里的指定的数值,会引发异常中断。
-
lgdt
所以的描述符都已经安装完毕,接下来的工作就是加载描述符的线性基地址和界限到GDTR寄存器,这要使用lgdt指令。
格式:
lgdt m48 ; lgdt m16&m32
也就是说,该指令的操作数是一个48位(6字节)的内存区域,在16位模式下,该地址是一个16位的,在32位模式下,该地址是32位的,该指令在实模式
和保护模式下都可以执行。
在初始状态下(0x00000000),界限值为0xffff. 该指令不影响任何标志位。
处理器的第21根地址线,编号A20,A0是第一根地址线,A31是第30根地址线。
端口0x92的位1用于控制A20,叫做替代的A20门控制(Alternate A20 Gate,ALT_A20_GATE),他和来自键盘的A20控制器一起,通过或门连接到处理器的
A20M#引脚。当INIT_NOW 从0过渡到1时,ALT_A20_GATE将被置1,也就是说,计算机启动时,第21根地址线是自动启动的。
0X92,第0位叫做INIT_NOW ,意思是现在初始化,用于初始化处理器,当他从0过渡到1时,ICH芯片会使处理器INIT#引脚的电平变低(有效),并保持
至少16个PCI时钟周期,通俗的说,向这个端口写1,将会使处理器复位,导致计算机重新启动。
控制保护模式和实模式之间切换的是一个叫做CR0的寄存器,为什么要在后面加一个0。因为还有其他的寄存器。
CR0是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。他的第一位(位0)是保护模式的允许位(protection enable,PE)
是开启保护模式大门的们把手,如果该位 “1”,则处理器进入保护模式,按保护模式的规则开始运行。
保护模式下的中断机制和实模式不同,因此,原有的中断向量表不再适用,而且必须要知道的是,在保护模式下,bios中断都不能在用,因为他是是
实模式下的代码。在重新设置保护模式下的中断环境之前,必须关中断。
;设置PE保护位
mov eax,cr0
or eax,1
mov cr0,eax
32位段寄存器,还包括了一个不可见的部分。称为描述符告诉缓存器。用来存放段的线性基地址,段界限和段属性。但是我们是不可见的.
即使在实模式下,段寄存器的描述高速缓存器也被用于访问内存,仅低20位有效,高12位全为零。当处理器进入保护模式之后,这些内容依然残留着,但是不影响使用,程序可以继续执行。
但是这些残留的内容在保护模式下是无效的。
在进入保护模式之前,有很多指令已经进入了流水线。
所以我们要清空流水线。
jmp dword 0x0008:flush ;dword 修饰的是偏移地址
不管用的是16位远转移,还是32位远转移,因为已经已经处于保护模式下,处理器会将第一个操作数0x0008视为段选择子,而不是实模式下的逻辑地址。
在保护模式下,不允许使用mov指令改变段寄存器cs的内容。
mov cs,ax ; 这个是不允许的,企图这样做将导致处理器产生一个无效操作码的异常中断。
mov cs,00000000000_11_000B; 加载堆栈选择子
对于向上扩展的段,段内偏移量是从0开始递增的,偏移量的最大值是界限值和粒度的乘积,而对于向下扩展的段来所,因为他经常用做栈段,而栈是从高地址想低地址
方向推进的,故段内偏移量的最小值是界限值和粒度的乘积加1。
栈操作时:
esp > 段界限*粒度值
mov cs,ax; 在保护模式下,这个是不允许的。
mov ss,cx ; 但是这个是可以的
在32位处理器上,每次向段向段寄存器传说逻辑段地址,处理器即在段及粗气描述符告诉缓存器中存放一个左移后的20位基地址。因此,即使在实模式下,处理器也是用
段寄存器描述符告诉缓存器的32位加上16位偏移地址访问内存.只不过基地址的高12位通常位0。
处理器第一次取指令时发出的地址是0XFFFFFFF0,之所以这样做,是因为处理器的设计者希望吧ROM-BIOS
放到4GB可寻址内存范围的最高段,这样,4gb以下,连同传统的的低端1mb都是连续的ram.
141.info gdt
显示dgtr的内容。 base 表示段的21位基地址,limit表示段界限值。execute-only表示只执行,non-conforming表示非依从的,32-bit表示这是一个32位的段.
142.creg(control register)用来查看控制寄存器的内容,bochs给出了各个控制位的状态,小写表示该位是0,大写表示该位是1,即处于置位的状态。
143.在保护模式下,对描述符有效与否的检查,通常只在加载段寄存器(选择器),并刷新描述符告诉缓存器的时候进行。
对于代码段来说,典型的例子是远转移或者过程调用。
jmp 0x0008:0x0002
对于数据段来说,典型的例子就是加载段选择子
mov ds,ax
当这类指令执行时,处理器用指令中给出的选择子找到描述符,如果描述符有效,就将选择子加载到段寄存器(选择器),并吧描述符加载到描述符高速缓存器。
144.处理器引入保护模式的目的是提供保护功能,其中很重要的一个方面就是存储保护,存储保护器的功能可以禁止程序的非法内存访问。
比如,向代码段中写入数据,访问段界限之外的内存位置等。很多时候,一旦能够及时发现和禁止这些非法操作,程序失去控制之前引发异常中断,就可以提高
软件的可靠性。
在32位处理器上,即使是在实模式下,也可以使用32位寄存器,所以,所以可以直接将gdt的32位线性基地址传送到寄存器eax中。
146.一个王了初始化的指针往往默认值往往就是0,所以空描述符的用意就是阻止不安全的的访问。
147.如果一个段的粒度为4kb,那么实际使用的段界限为(描述符中的段界限值+1)*0x1000-1 0x1000(4096)
148.在保护模式下,代码段是不可写入的,所谓不可写入,并非是说改变了内存的物理性质,使得内存写不进去,而是说,通过该段的描述符来访问这个区域时,
处理器不允许向里面写入数据或者更改数据。
如果需要访问代码段内的数据,只能重新为段安装一个新的描述符,并将其定义为可读可写的数据段,这样,当需要修改代码段内的数据时,可以通过这个新的描述符
来进行。
当两个以上的描述符都描述和指向同一个段时,吧另外的描述符称为别名(alias);
别名应用的例子:如果两个程序向共享一个内存区域,可以分别为每个程序都创建一个描述符,而且他们指向同一个内存段,这也是别名应用的例子。
149.如果检查到指定的段描述符号,其位置超过表的边界时,处理器中止处理,产生异常中断13。
150.只有可以写入的数据段才能加载到ss的选择器,cs寄存器只允许加载大妈段描述符,另外,对于ds,es,fs,gs的选择器,可以向其加载数值我而哦0的选择子。
尽管在加载的时候不会有任何问题,但是,真正要用来访问内存时,就会导致一个异常中断,这是一个特殊的设计,处理器用他来保证系统安全。
不过对于cs和ss的选择器来说,不允许向其传送为0的选择子。
151.假如实际使用的段界限是0x001FFFFF
可以认为,此数值是当前段内最后一个允许访问的偏移地址,任何时候,eip寄存器的内容加上取得的指令长度减1,都必须小于等于0x001fffff,否则将引起处理器异常中断。
152.和向高地址防线扩展的段相比,非常重要的一点就是i,实际使用的栈界限就是段内不允许访问的最低端偏移地址。
至于最高端的地址,则没有界限,最大可以是0xffffffff,也就是说,在进行栈操作时,必须符合一下规则:
实际使用的栈界限+1 <= (esp的内容-操作数长度) << 0xffffffff
153.操作数在压入栈的物理地址要用段寄存器的描述符告诉缓存器中的段基址和esp的内容相加得到。
对于其他隐式操作栈的指令,比如pop,call,ret等,情况也没什么不用,也要根据操作数的大小来检查
是否违反了段界限的约束,以防止出现访问越界的情况。
154.对于取指令来说,是否越界取决于指令的长度,而对于数据段来说,则取决于操作数的尺寸。
155.xchg 是交换指令,用于交换两个操作数的内容,源操作数和目的操作数都可以是8/16/32位的寄存器,或者是
指向8/16/32位实际操作数的内存单元,但不允许两者同时为内存地址。
格式:
xchg r/m8,r8
xchg r/m16,r16
xchg r/m32,r32
xchg r8,m8
xchg r16,m16
xchg r32,m32
注意:不允许源操作数和目的操作数同时为内存单元。
对代码段实施保护的意思是通过代码段描述符不能修改段中的内容,但不意味着通过其他描述符做不到。
对于向上扩展的段来说,段界限在数值上等于段的长度减去一。
bswap 是字节交换指令(byte swap),在标准的32位处理器上只允许32位的寄存器操作数
bswap r32
处理器执行指令时,按如下过程操作(DEST是指令中的操作数,TEMP是处理器内的临时寄存器)
TEMP <- DEST
DEST[7:0] <- TEMP[31:24]
DEST[15:8] <- TEMP[23:16]
DEST[26:16] <- TEMP[15:8]
DEST[31:24] <- TEMP[7:0]
cpuid 指令
用于返回处理器的标识和特性信息,eax用于指定要返回什么样的信息,也就是功能,有时候,还要用到ecx,cpuid指令执行后,处理器
将返回的信息放在eax,ebx,ecx或者edx中。
在32位处理器上,原先的标志寄存器flags也相应的扩展到了32位,一支持更多的标志,扩展之后的标志寄存器称为eflags寄存器,他的
id标志位(位21)如果为"0",则不支持cpuid指令,反之,该处理器支持cpuid指令。一般情况下,不需要检测处理器是否支持cpuid指令。
为了探测处理器最大能够支持的功能号,应该先用0号功能来执行cpuid指令:
mov eax,0
cpuid
处理器执行后,将在eax寄存器返回最大可以支持的功能号。
同时还在ebx,ecx,edx返回处理器供应商的信息。
用户程序必须符合规定的格式,才能被内核识别和加载,通常情况下,流行的操作系统会规定自己的可执行文件格式,一般比较复杂,这种复杂性和操作系统自身的
复杂性是信息相关的。
在早先的处理器中,转移指令是影响处理器速度的重大因素之一,因为他会使流水线中那些已经预取和译码的指令失败,在较晚的处理器中,普遍采用了分支预测技术,
但并不总能保证预测是准的。因此,最好的办法就是尽量不是用转移指令。
cmovcc指令是从p6处理器开始引入的。
条件转移指令和传送指令相结合的产物,既有条件转移指令的多样性,又执行的时候传送操作。
注意:
他的目的操作数只允许16位或者32位通用寄存器,源操作数只能是相同宽度的通用寄存器和内存单元。
要在GDT内安装描述符,必须知道他的物理地址和大小。而要知道这些信息,可以使用指令sgdt(store global descriptor table register),他用于将gdtr寄存器的基地址和边界信息保存到指定的内存位置
sgdt指令的格式为:
sgdt m
其中,m为一个6字节内存区域的首地址,该指令不影响任何标志位。低2字节用于保存gdt的界限(大小),高4字节用于保存gdt的32位物理地址。
movzx
作用:是在零扩展的传送(move with zero-extend)
指令格式:
movzx r16,r/m8
movzx r32/r/m8
movzx r32,r/m16
movzx指令的目的操作数只能是16位或者32位的通用寄存器,源操作数只能是8位或者16位的通用寄存器,或者是执行一个8位或16位内存单元的地址。
也就是说,目的操作数和源操作数的大小是不同的。
movsx 意思是带符号扩展的传送(move with sign-extension)
格式:
movsx r16,r/m8
movsx r32,r/m8
movsx r32,r/m16
和movzx不同,movsx在执行扩展时,用于扩展的比特取自源操作数的符号位。
mov al,0x08
movsx cx,al ; cx = 0x0008,因为al的最高位是0
mov al,0xf5
movsx ecx,al ; ecx = 0xfffffff5, 因为al的最高位是1
两个字符串做比较,可以使用cmps指令。
格式:
cmpsb ; 字节比较
cmpsw ; 子做比较
cmpsd ; 双子比较
在16位模式中,源字符串的首地址由ds:si指定,目的字符串的首地址由es:di指定;
在32位模式下,则分别是ds:esi 和 es:edi.在处理器内部,cmps指令的操作是吧两个操作数想减,然后根据结果设置标志寄存器中相应的标志位。
取决于标志寄存器EFLAGS中的df位,如果df= 0 ,标志正向比较。也就是按地址递增的方向比较,这些指令执行后,si(esi)和di(edi)的内容分别加1,加2,加4;
否则,如果df = 1,表明是反向比较。这些指令执行后,si(esi)和di(edi)的内容分别减1,减2和减少4。
单纯的cmps指令只比较一次,他属于推一下才动一动的那种类型,所以,需要加指令前缀rep使比较连续进行。
连续比较的次数由cx(ecx)寄存器控制。在16位模式下,使用cx寄存器,在32位模式下,使用ecx寄存器。
针对cmps指令,应当使用repe(reoz)和repne(repnz)前缀。
前者的意思是:若相等(为零)则重复,后者的意思是:若不等(非零)则重复。但无论是那种情况,总的比较次数由cx(ecx)控制。
rep 终止条件: 1. (e)cx=0 2.无
repz/repe 终止条件: 1.(e)cx = 0 2.zf= 0
repnz/repne 终止条件:(e)cx = 0 2.zf=1
repe/repz 用于搜索第一个不匹配的字节,字,双字。
repnz/repne 用于搜索第一个匹配的字节,字,双字。
为了有效地在任务之间实施隔离,处理器建议每个任务都应当具有自己的描述符表,称为局部描述符表LDT(local descriptor table),并且吧专属于自己的那个段放到LDT中。
和GDT一样,LDT也是用来存放描述符的,不用之处在于,LDT只属于某个任务,或者说,每个人物都有自己的LDT,每个任务私有的段,都应当在LDT中进行描述。
注意:LDT的第一个描述符,也就是0号槽位,也是有效的,可以使用的。
程序是记录在载体上的指令和数据,总是为了完成某个特定的工作,其正在执行中的一个副本,叫做任务。
在一个多任务的系统中,会有很多任务在轮流执行,正在执行中的那个任务,称为当前任务(current task);
由于LDTR寄存器只有一个,所以,它只用于指向当前任务的LDT,每当任务发生切换的时候,LDTR的内容被更新。
以指向新任务的LDT.和GDTR一样,LDTR包含了32位线性基地址字段和16位段界限字段,以指示当前LDT的位置和大小。
在一个多任务的环境中,当任务发生切换时,必须保护旧任务的运行状态,或者说是保护现场
保护的内容包括,通用寄存器,段寄存器,栈指针寄存器esp,指令指针寄存器eip,状态寄存器eflags,等等。
为了保存任务的状态,并在下次重新执行时恢复他们,每个任务都应当应用一个额外的内存区域保存相关信息,这叫做任务状态栏(task state segment) tss
处理器TR寄存器来指向当前任务的TSS,和GDTR,LDTR一样,TR寄存器在处理器中也只有一个,当任务切换的时候,TR寄存器的内容也会跟这指向新任务的TSS;
过程:首先,处理器将当前任务的现场信息保存到由TR寄存器指向的TSS,然后,在使TR寄存器指向新任务的TSS,并从新任务的TSS中恢复现场。
为什么这个寄存器叫TR,而不是TSSR,原因很简单,TSS是一个人物存在的标志,用于区别一个任何和其他任务,所以,这个寄存器叫做任务寄存器(Task Register:TR).
每个任务实际上包括两个部分:全局部分和私有部分,全局部分是所有任务共有的,含有操作系统和软件和库程序,以及可以调用的系统服务和数据,私有部分则是每个人物各自的数据和代码
与人物所要解决的问题相关。
任务实际上是在内存中运行的,所以,所谓的全局部分和私有部分,其实是地址空间的划分,即全局地址空间和局部地址空间,简称全局空间和局部空间。
特权级,也叫特权级别,是存在于描述符以其选择子中的一个数值,当这些描述符或者选择子所指向的对象要进行某种操作,
或者被别的对象访问时,该数值用于控制他们所能进行的操作,或者限制他们的可访问性质。
intel 处理器可以识别4个特权值,分别是0到3,较大的数值以为着较低的特权级别。
对于数据段来说,dpl决定了他们所应当具备的最低特权级别,如果有一个数据段,其描述符的dpl字段为2,那么只有特权级为0,1,2的程序才能访问它。
当一个特权级为3的程序试图去读写该段的时候,将会被处理器阻止,并引发异常中断。
177.当任务在自己的局部空间内执行时,当前特权级cpl是3,当他通过调用系统服务,进入操作系统内核,在全局空间执行时,当前特权级cpl就变成了0。
总是,很重要的一点,不能僵化地看待任务和任务的特权级别。
178.计算机的脆弱性在于一条指令就能改变他的整体允许状态,比如停机指令hlt和对控制寄存器CR0的写操作,像这样的指令只能由最高特权级别的程序来做。
因此,那些只有在当前特权级cpl为0时才能执行的指令,称为特权指令。
除了那么特权级别敏感的指令外,处理器还允许对各个特权级所能执行的io操作进行控制。通常,这指的是端口访问的许可权,因为对设备的访问都是通过端口进行的,
在处理器的标志寄存器eflags中,位13,位12是iopl位,也就是输入/输出特权级别(i/o oribilege level),他代表这当前任务的i/o特权级别。
任务是由操作系统加载和创建的,与任务相关的而信息都在他自己的任务状态段(tss)中,其中就包括一个eflags寄存器的副本,用于指示与当前任务相关的机器状态,比如他
自己的io特权值iopl,在多任务系统中,随着任务的切换,前一个任务的所有状态被保存到他自己的tss中,新人物的各种状态从其tss中恢复,包括eflags寄存器的值。
代码段的特权级检查是很严格的,一般来说,控制转移只允许发生在两个特权级相同的代码段之间,如果当前特权级为2,那么,他可以转移到另一个dpl为2的代码段接着执行。
但不允许转移到dpl为0,1和3的代码段执行。
为了让特权级低的应用程序可以调用特权级高的操作系统例程,处理器提供了相应的解决办法。
第一种方法:将高特权级的代码段定义为依从段。代码描述符的TYPE字段有c位,如果c = 0,这样的代码只能供同特权级的程序使用,否则,如果c=1,
则这样的而代码段称为依从的代码段,可以从特级比他低的程序调用并进入。
在任何时候,都不允许将控制从较高的特权级转移到较低的特权即被。
使用jmp far指令,可以将控制通过门转移到比让前特权级高的代码段,但不改变当前特权级别,但是,如果使用call far 指令,则
当前特权级会提升到目标代码段的特权级别,除了从高特权级别的例程(通常是操作系统例程返回外),不允许从特权级高的代码段将
控制转移到特权级别低的代码段,以为操作系统不会引用可靠性比自己低的代码。
特权级保护机制只在保护模式下才能启用,处理器建议,在进入保护模式后,执行的第一条指令是跳转或者过程调用指令,
以清空流水线和乱续执行的结果,并穿行化处理器。
在进入保护模式之后,处理器自动将当前特权值cpl设定为0,以0特权级的身份开始执行保护模式的初始指令。
每当处理器执行一个段选择子传送到段寄存器(ds,es,fs,gs)的指令,
比如:
mov ds,cx
时,会检查以下两个条件是否都满足。
当前特权级cpl高与或者和数据段描述符的dpl相同,在数值上,cpl<= 数据段描述符的dpl
请求特权级rpl高于或者和数据段描述符的dpl相同,即,在数值上,rpl <= 数据段描述符dpl
如果以上两个条件不能同时成立,处理器就会阻止这种操作,并引发异常中断。
最后,处理器要求,在任何时候,栈段的特权级别必须和当前特权级cpl相同,因此,随着程序的执行,要对段寄存器ss的内容进行修改,
必须进行特权级检查,以下就是一个修改段寄存器ss的例子。
mov ss,ax
要求:
在数值上: cpl = 目标段栈描述符的dpl
rpl = 目标段描述符的dpl
所以,处理器的设计者建议,如果不需要使用特权机制的话,可以把所有程序的特权级别都设置为0,就像我们一直所想的那样。
除了调用门返回外,不允许将控制转移到低特权级别的局部空间内的代码段。
在任何时候,当前栈的特权级别必须和cpl是一样的,进入不用特权级别的段执行时,要切换栈。
如果是代码段,则通常只有0特权级的程序才能控制转移到该段,也就是说,只能从内核其他正在执行的部分转移到该段执行,因为他们的特权
别相同。
190.例程是由内核提供的,他们的特权级别通常就是内存的特权级。
191.任何操作系统都应当提供大量的功能调用服务,为此,需要安装调用门。调用们用于在不同特权级别的程序之间进行控制转移,本质上,它只是一个吗描述符
,一个不用于 代码段和数据段 的描述符,可以安装在gdt或者ldt中。
这个描述符中包含了例程所在代码段的选择子,有了选择子,就能访问描述符表得到代码段的基地址,这样做无非是简介了点,
但却可以在通过调用门进行控制转移时,实施代码段描述符有效性,段界限和特权级的检查。
例程在代码段中的偏移量也是在
描述符的TYPE字段用于标识门的类型,共4比特,值"1100" 标识调用门。
描述符中的P位是有效位,通常应该是”1“,当他为0的时候,调用这样的门会导致处理器产生异常中断。
对于因为 p 位为0而引起的中断来说,他们属于故障中断,从中断处理过程返回时候,处理器还会重新执行引起故障的指令。
调用们描述符中还有一个参数个数字段,用5比特,就是说,至多允许传送31个参数。
调用门描述符中的dpl和目标代码段描述符的dpl用于决定那些特权级程序可以访问此门。
具体的规则:
1.
cpl <= 调用门描述符的dpl
rpl <= 调用门描述符的dpl
2.
cpl >= 目标代码段的dpl
如果调用门描述符的dpl为2,那么,只有特权为0,1和2程序才可以使用调用门。
特权值为3的程序使用此们会引发处理器异常中断。
调用门描述符中一些字段没有使用,固定为"0"。
调用们实施特权之间的控制转移时,可以使用jmp far 指令,也可以使用call far指令,如果是后者,会改变当前特权值cpl,
因为栈段的特权值必须同当前特权值保持一致,因此,要切换栈。
切换栈的目的:主要是为了防止因栈空间不足而产生不可预料的问题,同时也为了防止栈数据的交叉引用。
任务寄存器TR总是指向当前任务的人物状态段TSS,其内容为该TSS的基地址和界限,在切换栈时,处理器可以用TR找到当前人物的TSS,并从TSS中获取新栈的信息。
通过调用门使用高特权级的例程服务时,调用者会传递一些例子给例程,如果通过寄存器传送,这没什么可说的,不过,要传递的从那数很多时,
更经常的做好是通过栈的进行,调用者吧参数压入栈,例程从栈中取出参数。
call far [salt_1 + 256]
处理器在执行这条指令时,会用该选择子访问GTD/LDT,检查那个选择子,看他执行的是调用门描述符,还是普通的代码段描述符,如果是前者,就
按照调用门来处理,如果是后者,还按一般的段间控制转移来处理。
mov edx,[ebp]
在32位模式下,处理器执行这条指令时,用段寄存器ss描述高速缓存器的32位基地址,加上ebp寄存器提供的32位偏移量,形成32位线性地址,访问内存取得一个双字,传送到
edx寄存器,用ebp寄存器来寻址时,不需要使用段超越前缀ss: ,因为ebp寄存器出现在指令中的地址部分,默认使用段寄存器ss.
当用户程序被读入内存,并处于或者等待运行的状态时,就视为一个任务
mov esi,[ebp+11*4] ; 默认使用段寄存器ss来访问内存。因为使用ebp
ldl,每个任务一个,所以,为了追踪他们,处理器要求在gdt中安装每个ldt的描述符,当要使用这些ldt时,可以用他们的选择子来访问gdt,将ldt描述符加载到
ldtr寄存器。
tss内偏移0处是前一个任务的tss描述符选择子,和ldt一样,必须在全局描述符(gdt)中创建每个tss的描述符。当系统中有多个任务同时存在时,
可以从一个人物切换到另一个任务执行,此时成任务是嵌套的。被嵌套的任务用这个指针指向前一个人物,即嵌套他的那个任务,当控制返回前一个
任务时,处理器需要这个指针识别前一个任务
创建tss时,可以是0
注意:界限值必须至少是103,任何小于该值的tss,在执行任务切换的时候,都会引发处理器异常中断。
EFLAGS寄存器的IOPL位决定了当前人物的I/O特权级别,如果当前特权级CPL高于,或者和任务的IOPL相等时,即,在数值上,
CPL <= IOPL 时,所有的I/O操作都是允许的,针对任何端口的访问都可以通过。
I/O许可串是一个比特序列,或者说是一个比特串,最多允许65536比特,即8kb,从第一个比特开始,各比特用他的串中的位置代表一个端口串,
一次最多65536比特,65536个端口,每个比特的取值决定了相应的端口是否允许访问,为1时,禁止访问,为0时,允许访问。
映射区最后一个字节的所有比特必须都是1,即,最后一个字节是0xff。
如果要检查的比特在最后一个字节中,那么,这个两字节的读操作将会越界,为防止这种情况,处理器要求I/O许可映射区的最后必须附加一个
额外的字节,并要求他的所有比特都是“1”,即0XFF,当然,他必须位于TSS的界限之内。
任务状态栏tss的最小长度是104字节,保存着最基本的任务信息,但这并不是他的最大长度。事实上,整个tss还包括一个I/O许可串,他所占用的区域称为I/O许可映射区。
在tss内偏移为102的那个字单元,保存着I/O许可串(I/O许可位许可映射区)的其实地址,因此,如果该字单元的内容大于或者等于TSS的段界限(在TSS描述符),则表明没有
I/O特权级IOPL,执行任何硬件I/O指令都会引发处理器异常中断。
和LDT一样,必须在GDT中创建TSS的描述符,TSS描述符包括了TSS的基地址和界限,该界限值包含I/O许可映射区在内。
存在将标志寄存器入栈和出栈的指令:
pushf/pushfd
popf/popfd
[bits 16]
pushf ;编译后9c,16位操作
pushfd ; 编译后66 9c.32位操作
[bits 32]
pushf ; 编译后是9c,32位操作
pushfd ; 编译后同样是9c,32位操作。
将指向前一个任务的指针(任务链接域)填写为0,表明这是唯一的任务。
和局部描述符(ldt)一样,也必须在gdt中安装tss的描述符,这样来,一方面是为了对tss进行段和特权级的检查,另一方面,也是执行任务切换的需要,当call far和jmp far
指令的操作数是tss描述符选择子时,处理器执行任务切换操作。
任务是不可重入的,就是说,在多任务环境中,如果一个任务是当前任务,他可以切换到其他任务,但不能从自己切换到自己。
在过程的返回时,顺便弹出参数
ret imm16 ; 近返回
retf imm16 ; 远返回
这两条指令都允许16位的立即数作为操作数,不同之处在于,立即数是16位的,而且一般总是偶数,原因是栈操作总是以字或者双字进行,
他指示在将控制返回到调用者之前,应该从栈中弹出多少字节的数据。
依从的代码段不是在它的dpl特权级上运行,而是在调用程序的特权级上运行,也就是说,当控制权转移到依从的代码段后,不改变当前特权cpl,段
寄存器cs的cpl不发生变化,被调用过程的特权依从调用者的特权级,这就是为什么他被称为依从的代码段。
当使用jmp far 指令通过调用门转移控制时,要求当前特权级和目标大妈段的特权级相同,原因是jmp far 指令通过调用门转移控制时,不改变
当前特权级cpl。
相反,使用call far 指令可以通过调用门将控制转移到较高特权级别的代码段,之所以说"可以",是因为,如果目标代码是依从的,则和jmp far一样,
不改变当前特权级,否则,如果目标代码是非依从的,则在目标代码段的特权级上执行。
其次,当使用call far 指令通过调用门转换控制时,如果改变了当前的特权级,则必须切换栈。
从当前任务的栈切换到目标代码特权级相同的栈上,栈的切换是由处理器固件自动进行的。