看雪的朋友门我来鸟。。
在编程区里:看给位大侠将的什么中断,内核,听得我直发毛。。
因为有很多专业词汇,内核知识。。从来没听过不知道大侠们在讲什么!!!
那么本人不甘心 。我就从头开始学吧
..现在那点笔记出来跟大家分享一下!!!
由于是笔记,也许记得有点糟糕!!
呵呵,来看雪总想发点东西,!!请见谅啊!!!
【保护模式编程、一】
【8086模式编程】
如果想更深、更亲近的了解电脑软件。
那么学习cpu是你的必选!!
386是CPU史的一大转折点,那386做基础课是最好不过了。那么我们将开始进行学习之旅!!!
大家跟我一块学习吧,呵呵!!!
一、准备工作
1、NASM 编译环境
2、虚拟机
Virtual PC(Windows平台 ,执行比较快,即模拟又虚拟硬件)、
WMWarve(WIndows平台 虚拟硬件,)、
Bochs(支持Windows平台、也支持在Linux平台上运行 有RPM版本的)
我们这些生长在Windows这棵大树下的朋友们,还是用Virtual PC吧.。
3、写虚拟启动镜像文件的程序
:不知道我观察的对不对?
用Nasm 编译一个bin 然后将它转换为img 镜像文件的时候。
只要文件大小符合软驱的标准就能启动。那么就代表a.bin 与a.img 文件的内容一模样就是文件大小不一样!
我是不太了解镜像文件格式.我用的是Virtual PC。
二、开始接触引导程序
1、Com文件
Com文件是纯二进制的文件,也是直接与Cpu交换的顺序指令文件。
Com文件的大小是有限制的,不能超过64KB.
因为8086时代的CPU地址线是20位的,20位能表达的数值也就是fffffh(1MB )
而寄存器最高也只是16位,无法用5个F的形式来表达地址,所以用CS(段基地址)*16:IP(偏移地址)来寻址!
80386后通用寄存器都得到了32位扩展! 而Cpu地址线也得到了32位的扩展。
引导程序前期是需要进入实模式的,因为这是硬件上的限制是IA32的限制。
386cpu只有两种模式: 实模式与保护模式!!!!,,
2、引导程序
引导程序也是有限制的,这个限制是靠Bios处理的,
开机后Bios经过自检后,会从软驱或者硬盘的0面0磁道1扇区搜寻一个程序文件。
该文件的数据必需是等于512Byte,并且以aa55h结尾的(高高低低)。那么bios会认为它是引导程序,
这个时候就会把该512byte 装载到内存7c00开始处。然后将主控权交给程序的第一行代码。
那么这个时候程序脱离Bios的控制。Cpu将执行程序的代码.
三、写一个引导程序
引导程序可以说是非常简单:
1、boot.asm(nasm 的源文件如下)
;-----------------------欲编译,这里改成100h就是com程序 -------------------------------------------
%define _BOOT_DEBUG_ ;做调试的时候用100h
%ifdef _BOOT_DEBUG_
org 0100h
%else
org 07c00h ; 告诉编译器 以下代码段将从07c00h内存地址处开始
%endif
mov ax,cs ;让数据段与附加段寄存器跟代码段一样,因为COM代码数据是混合.
mov ds,ax
mov es,ax
call _HelloWorld ;让程序显示一个HelloWorld
jmp $ ;$表示当前地址 无限循环
_HelloWorld:
mov ax,strHello ;取得字符串的地址
mov bp,ax ;给堆栈基寄存器
mov cx,strLen
mov ax,1301H ;ah代表功能号
mov bx, 000ch ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮
mov dx,0001h ;显示的行与列
int 10h ;bios 10h显示中断
ret
strHello: db "Hello World"
strLen equ $ - strHello
times 510-($-$$) db 0 ; times重复定义 510-($-$$)个 $$ 表示段的起始地址
dw 0xAA55
那么引导程序完成了,
用nasm boot.asm -o a.com
就可以运行看效果.,
如改成引导程序只需把%define _DEBUG_BOOT_注释 然后nasm boot.asm -o a.bin
然后用工具将a.bin 转换成软驱大小的镜像文件 载入虚拟机启动就可以.
【保护模式编程、二】
【80386保护模式编程】
8086到80386的跳转,80386与8086在硬件上的区别在这就不说了!!
那么80386与8086在软件逻辑上面的区别就是:
8086是实模式,而80386 不仅包括实模式,而且还可以进入保护模式!!!
保护模式不仅不受64KB内存寻址的限制,而且还拥有4GB的寻址空间。这是因为386扩展了20地址线,将它扩展成32位了(32位能表达的字节数就是4GB).
此时的段寄存器不再是段基地了,而被叫做是选择子 ,存放的是一个段描述符的索引值.
而我们的通用寄存器与EIP 也是32位的,可以表达4GB地址!
不过计算机开机后,CPU默认是实模式。这就需要我们编程手动转换到386.
那么我们该怎么去做呢:
一、【准备GDT (全局描述符表)】
首先我们需要准备GDT结构体,它是386保护模式必须的东西。
全局描述符寄存器GDTR 指向的是所有段描述符表的信息.
前面提到得段选择子索引,指得就是指向段描述符的索引.
段描述符是8个字节的结构体、里面存放着段的段界限、段基址、段属性等信息.
LDTR寄存器是指向局部某一个段的描述符表。
段描述符表结构体用一个宏来表示(注意段1 2表示同一个段描述内容被分开来放的):
【段界限】20位、表示段的总长度 这里并不是地址,而是段的字节长度。
【段基址】32位、表示物理地址
【段属性】12位. 系统、门、数据等属性
%macro Descriptor 3 ; 有三个参数:【段界限】、【段基址】、【段属性】
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 1 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 2 (1 字节)
%endmacro ; 共 8 字节
看似很简单的结构体 理解起来可不是那么简单!
【Descriptor结构体】有8个字节。
1、【第1、2字节】组合(word) 表示该段的[段界限①], dw %2 & 0FFFFh ;引用第二个参数去掉高16位
2、【第3、4、5字节】组合表示该段的[段基址①],dw %1 & 0FFFFh ;先得到第一个参数(段基址)低WORD。
3、接着把第5个字节赋值,db (%1 >> 16) & 0FFh 去掉第3第4个字节的内容.再把剩下的字节赋值
4、【第6个字节】是与【第7个字节】组合的内容可就更复杂了:
【第6个字节】的内容:
【7(p) 6(DPL) 5(DPL) 4(S) 3(Type) 2(Type) 1(Type) 0(Type)】
0-3位表示:[段属性]、说明存储段描述符所描述的存储段的具体属性。
4位表示:说明描述符的类型, 对于存储段描述符而言,S=1表示是系统段描述符。
5-6位表示:DPL 该段的特权级别也就是Ring 0-3;
7位表示:P: 存在(Present)位。
; P=1 表示描述符对地址转换是有效的,即描述的段在内存当中.
; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常
【第7个字节】的内容:
【7(G) 6(D) 5(0 ) 4(AVL) 3(段界限) 2(段界限) 1(段界限) 0(段界限)】
0-3位表示:[段界限②]
4位表示:软件可利用位。80386对该位的使用未做规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。
5位表示:0 ;Intel资料也没表示
6位表示:是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同,通常置1
7位表示: 段界限粒度(Granularity)位。
G=0 表示界限粒度为字节;
G=1 表示界限粒度为4K 字节。
注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
那么这段宏dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)表示:
取[段界限]参数除去低16位取 高4位,得到【段界限②】
取[段属性]参数的低8位 12-15位(AVL属性等)
属性 1 + 段界限 2 + 属性 2
【第8个字节】的内容:
[段基址②] 、db (%1 >> 24) & 0FFh 取基地址参数的最高8位
那么一个Descriptor 结构体就这样成形了.
二、【编写程序跳转到保护模式】
%include "386.inc" ;是Descriptor结构体宏
;%define _DEBUG_BOOT_
%ifdef _DEBUG_BOOT_
org 0100h
%else
org 07c00h
%endif
jmp LABEL_BEGIN
[SECTION .gdt] ;全局描述符数据段
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ;空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_CR | DA_32 ;代码段描述符
LABEL_DESC_DATA: Descriptor 0,SegDataLen - 1,DA_DRW ;数据段
LABEL_DESC_VIDEO: Descriptor 0B8000h,0FFFFh,DA_DRW ;显示器内存段 由于DOS中断不能随意使用了,,只能输出到显示缓冲区
; GDT 结束
GdtLen equ $ - 1
GdtPtr dw GdtLen - 1 ;GDT 的段界限,
dd 0 ;GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ;代码相对全局描述符起始地址的EA值
SelectorData equ LABEL_DESC_DATA - LABEL_GDT ;数据段
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ;显示数据段
[SECTION .s16] ;16位代码段
[BITS 16] ;BITS指出处理器的模式 是16位
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax ;初始化段寄存器
;初始化数据
mov eax,strHello
mov word[LABEL_DESC_DATA + 2],ax
mov byte[LABEL_DESC_DATA + 4],al
shr eax,16
mov byte[LABEL_DESC_DATA + 7],ah
; 初始化并把32位段代码的基地址分配给段描述符
mov eax, LABEL_CODE32 ;
mov word [LABEL_DESC_CODE32 + 2], ax ;ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 为加载 GDTR 作准备
mov eax, LABEL_GDT
mov dword [GdtPtr + 2], eax ;得到GDT基地址
; 加载 全局描述符的信息结构体 到GDTR
lgdt [GdtPtr]
CA20 ;利用键盘端口打开A20地址线
; 将CRO的PE位 也就是 0位 置1 那么就进入386模式了
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword SelectorCode32:0 ;
;执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;这个描述符集合是以一个空描述符开始得,现在LABEL_DESC_CODE32描述符的索引值因该是8,
;所以SelectorCode32的值应该就是LABEL_DESC_CODE32的索引值,Code32Selector:0当中的0是指LABEL_DESC_CODE32 的段基址+ 0
;那么在打开cr0的PE位后,这个JMP指令不再是直接跳到段地址去了;
;而是去GDTR全局描述符寄存器当中去找这个当前CS的索引,当前段基址+偏移 的内存地址了。
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_CODE32:
;保护模式的死循环
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 10 + 9) * 2 ; 屏幕第 10 行, 第 0 列。
mov ah, 1Ch ; 0000: 黑底 1100: 红字
mov esi,0
mov ds,SelectData
mov ecx,11
vi:
lodsb
mov [gs:edi], ax
inc edi
LOOPNZ vi
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_CODE32
[SECTION .data] ;数据段
strHello: db "Hello World"
SegDataLen equ $- strHello
【总结】
编写一个386 程序主要用的步骤
1、准备GDT描述符集合结构体
2、用lgdt [gdtPtr] 载入 gdtPtr 这6个字节的结构体,,低字是描述符集合的界限 也就是集合总长度,高双字是描述符集合的基地址.
3、打开A20地址线
有一种方法是向键盘端口 IO,
4、置CR0的PE位 即0位为1
5、JMP [段索引]:[段基址偏移]
呵呵 接下来继续学习啊!!!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!