3. 加载器与加载连接用户程序
加载器与加载连接用户程序
- 加载器
- 创建用户程序的呆子内核
- 用户程序
加载器加载32位呆子内核
;MBR本身占用512字节,起始地址0x7c00
;GDT的位置:0x7c00+0x200(512) = 0x7e00
;GDT的界限:2的16次方:2^16=64k=0x10000
;因此启动程序的位置放在: 0x7e00+0x10000 = 0x17e00
;启动程序所在的逻辑扇区固定在0x01
;GDT界限:2^16, 每个段描述符8个字节,最多可容纳8192个段描述符
;每当加载一个段描述符伪指令: mov es,10_000 ;
;就会根据提供索引*8+GDT基地址 去获取此地址的段描述符,如果地址超出了(GDT基地址+GDT界限)则非法
;之前已经说过段界限的计算方式,这里再说明一下
;向上的段界限:指的是最大偏移 , 向下的段界限:特指<栈>, 是最小偏移
;段界限是一个数量单位,需要根据G位来计算实际的段界限,G:0 以字节为单位, G:1 以4k为单位
;拿下面的显存段描述符来说明:界限:0xffff,G=0. 段界限: (0xffff+1)*1-1,最终的段界限:段基地址+段界限:0xb8000 + (0xffff+1)*1-1
;下面栈段,段基址:0x7c00,段界限:0xffffe,G=1. 段界限:(0xffffe+1)*4096-1+1, 最后需要加上段基址:0x7c00+( (0xffffe+1)*4096-1+1 );
;section.段名.start 是从文件首(位置0)开始计算的实际物理段地址,相当于从内存地址0开始的物理段地址
;在实模式中,有20位的地址空间
; 1.段地址(section.段名.start)需要4个字节来存放,低20位有效位,假设段地址:0x1000
; 2.如果要计算逻辑段地址,则需要加上程序起始地址, 假设程序起始地址:0x10000
; 1.起始地址:0x10000 低2字节 + 段地址 低2字节 : 0x0000 + 0x1000 = 0x1000
; 2.起始地址:0x10000 高2字节 + 段地址 高2字节 : 0x0001 + 0x0000 = 0x0001, 此加法使用adc,别忘记进位
; 3.mov ax,0x1000, mov dx,0x0001
; 3.逻辑段地址 / 16, 因此低2字节: shr ax,4 = 0x100
; 5.高2字节中只有低4位是有效位(共20位), or ax,dx => 0x1100
;在32位保护模式中,有32位地址空间.物理段地址需要4字节存放
;保护模式中不需要计算逻辑段地址,而是要创建一个描述符
;描述符的基地址: section.段名.start(内存地址为0开始的物理地址) + 程序起始地址: section.段名.start+CORE_BASE_ADDR;
;段界限:可以在每个段的结尾增加一个标号来计算每个段的长度, 根据段长度-1 => 界限
;段属性:根据每个段是什么类型,自己在头部给出
;根据上述3个属性可以合成一个描述符
MBR_START_ADDR equ 0x7c00 ;MBR起始地址
CORE_BASE_ADDR equ 0x17e00 ;启动程序的起始地址
CORE_SECTOR equ 0x01 ;启动程序所在的扇区
SECTION mbr vstart=0
;实模式
;首先在GDT中创建一些需要用到的段描述符
;GDT_BASE 被定义成一个4字节的线性地址
;获取GDT的段地址,偏移地址
mov ax,[cs:GDT_BASE + MBR_START_ADDR] ;获取GDT低16位
mov dx,[cs:GDT_BASE + MBR_START_ADDR + 2] ;GDT高16位
mov bx,16
div bx
;ax:段地址, dx:偏移
mov ds,ax ;GDT段地址: 0x07e0
mov bx,dx ;偏移:0
;创建第一个描述符:哑元,0号描述符
;每个段描述符占用8个字节
mov dword [ds:bx + 0x00],0
mov dword [ds:bx + 0x04],0
;创建一个代码段描述符, 基地址:0x7c00, 界限:0x01ff , G=0 字节为粒度, D=1 使用32位操作数
;实际最大界限: 0x7c00 + ((0x01ff + 1)*1-1)
;此段地址0x7c00,与MBR起始地址一样, 主要是为了执行后面的[bits 32]编译的32位保护模式下的代码
mov dword [ds:bx + 0x08] , 0x7c0001ff
mov dword [ds:bx + 0x0c] , 0x00409800
;创建一个显存段描述符,用于输出
;段基地址:0xb8000,界限:0xffff,G=0, 64k
;实际最大界限: 0xb8000 + ((0xffff+1)*1-1)
mov dword [ds:bx + 0x10], 0x8000ffff
mov dword [ds:bx + 0x14], 0x0040920b
;创建一个指向4G内存的数据段,以便访问任何一个位置
;段基地址:0x0, 界限:0xfffff, G=1(以4K为单位)
;实际最大界限:0x0 + (0xfffff+1)*4k-1
mov dword [ds:bx + 0x18],0x0000ffff
mov dword [ds:bx + 0x1c],0x00cf9200
;创建栈段
;段基址: 0x7c00, 段界限:0xffffe,G=1 以4K为单位, B=1 32位操作数,即使用esp
;栈的段界限指的是最小偏移
;段界限:(0xffffe+1)*4096-1+1 = 0xFFFFF000
;实际最终段界限: 0x7c00 + ( (0xffffe+1)*4096-1+1 ) = 0x7c00 + 0xFFFFF000 = 0x6c00
;最小偏移:0x6c00 , 最高地址: 0x7c00 + 0xffff_ffff(esp) = 0x7bff
;每当 push ,pop, int, iret ,call 等需要操作栈的指令时,都会对越界进行检查
;例如: push eax
;需要检查: 最小偏移地址 <= (esp - 4)
mov dword [ds:bx + 0x20],0x7c00fffe
mov dword [ds:bx + 0x24],0x00cf9600
;设置GDT的界限
mov word [cs:GDT_SIZE + MBR_START_ADDR], 39 ; 5个段描述符 : 5*8 -1
;设置GDTR, 共6个字节,低2字节GDT_SIZE, 高4字节GDT_BASE
lgdt [cs:GDT_SIZE + MBR_START_ADDR]
;打开A20地址线
;使用0x92端口,把此端口的位1置1
in al,0x92
or al,0x02
out 0x92,al
;屏蔽中断
cli
;开启CR0的PE位,PE位在位0
mov eax,cr0
or eax,1
mov cr0 ,eax
;一旦开启PE位,当前在16位保护模式下运行了
;当前在16位保护模式中的D位还是0, 想进入32位保护模式,需要加载一个D=1的段描述符,因此需要使用jmp
;dword 用于修饰偏移地址:into_protect_mode, 使用4字节的偏移地址
;由于当前还是16位,因此会在机器码前加上前缀0x66, 运行时会使用32位操作数
;0x08是段选择子: 01_000B, 段选择子2个字节,高13位是索引,2^13=8192,对应GDT最大描述符个数
;段选择子传递给cs的过程,伪指令: mov cs, 0x08:
; 1.根据 ( 索引 * 8 ) + GDT_BASE 算出地址
; 2.查看此地址是否越界( GDT_BASE + GDT_SIZE)
; 3.未越界则把对应描述符加载到cs的高速缓冲区中
;以下面为例:
; 1. 0x08 = 01_000B, 索引为1
; 2. (1*8)+0x7e00 = 0x7e08, 不越界
; 3. 从0x7e08处获取8个字节加载到cs高速缓冲区中
;
;相当于伪指令: mov cs,0x08 ; mov eip, into_protect_mode
;最后由于段描述符被加载后D=1,因此使用eip, 即 eip = into_protect_mode
jmp dword 0x08:into_protect_mode
;从这里开始将使用32位的操作数,由于当前的CS段描述符D=1
;bits 32 以32位编译代码, 默认情况是bits 16以16位编译
;如果当前没有bits 32的情况下, 而描述符的D位又为1, 即以32位方式执行16位的指令或许会发生错误
;16位的默认操作数是2字节,32位默认操作数为4字节. 即使有些编译好的指令看上去一样,但在运行时还是会发生错误
[bits 32]
;以32位操作数来执行指令
into_protect_mode:
;设置栈段
mov eax,0x20
mov ss,eax
xor esp,esp
;设置数据段,指向4G空间
mov eax,0x18
mov ds,eax
;读取启动程序第一个扇区,加载到CORE_BASE_ADDR处
mov ebx,CORE_BASE_ADDR ;写到ds:ebx
mov edi,ebx ;备份
mov eax,CORE_SECTOR ;指定起始扇区号
call read_sector ;读取首个扇区, 参数:eax , 写入到 ds:ebx
mov eax,[ds:edi] ;获取程序长度
mov ecx,512
xor edx,edx
div ecx ;总字节数/512, 查看还要读取几个扇区
or edx,edx ;余数为0 ?
jnz .check
dec eax ;余数为0, -1首个已读的扇区
.check:
or eax,eax ;是否读完 ?
jz .read_done ;读完则处理一步,否则继续读扇区
mov ecx, eax ;剩余扇区数
mov eax, CORE_SECTOR
.read_left:
inc eax ;下一个扇区号
call read_sector ;读取扇区, ebx在read_sector中自增,因此不用管
loop .read_left
;全部读完
.read_done:
;为所有的段创建段描述符
;程序的重定位表中已经把 段基址, 段界限, 段属性 全部提供
;程序被加载的位置:CORE_BASE_ADDR
;因此实际的段基址为: CORE_BASE_ADDR + 段基址
;当前ds:指向4G
mov ecx,[ds:CORE_BASE_ADDR + 0x0c] ;重定位表长度(有几项)
mov edi,0x10 ;重定位表起始地址
;保存ebp 原值
push ebp
mov ebp,0x28 ;安装描述符的起始地址,实模式中最后一个地址:0x20
;处理重定位表
;这里的ebp:把描述符的地址当成段选择子来使用,只是凑巧,因为后3位(TI,DPL)都是0.
.process_realloc_table:
mov ebx,[ds:CORE_BASE_ADDR + edi] ;段界限
mov eax,[ds:CORE_BASE_ADDR + edi + 4] ;段基址
add eax,CORE_BASE_ADDR ;实际段基址
mov esi,[ds:CORE_BASE_ADDR + edi + 8] ;段属性
;合成描述符
call make_gd ;返回edx:eax
;增加到GDT中
mov esi,[ds:MBR_START_ADDR + GDT_BASE] ;GDT起始位置
mov [ds:esi + ebp],eax ;描述符低32位
mov [ds:esi + ebp + 4],edx ;描述符高32位
;把程序头部的段基址替换成段选择子
mov [ds:CORE_BASE_ADDR + edi + 4],ebp
add ebp,0x08 ;下一个安装段描述符的位置
add edi,0x0c ;下一个段项
loop .process_realloc_table
;恢复ebp
pop ebp
;修改入口点的段基址,替换成段选择子
mov eax,[ds:CORE_BASE_ADDR + 0x20]
mov [ds:CORE_BASE_ADDR + 0x08],eax
;传递显存段,栈段,4G数据段的段选择子
mov dword [ds:CORE_BASE_ADDR + 0x40],0x18 ;4g数据段
mov dword [ds:CORE_BASE_ADDR + 0x44],0x20 ;栈段
mov dword [ds:CORE_BASE_ADDR + 0x48],0x10 ;显存段
mov dword [ds:CORE_BASE_ADDR + 0x4c],0x08 ;MBR段
;修改GDT段界限
mov word [ds:MBR_START_ADDR + GDT_SIZE], 71 ;9*8-1
;重载GDT,使其生效
lgdt [ds:MBR_START_ADDR + GDT_SIZE]
;跳转到内核程序中,此程序作废
;CORE_BASE_ADDR + 0x04 是目标程序的入口点, 存放了偏移地址,段选择子
;jmp far 用于段间转移, 将从目标地址处获取6个字节,低地址:偏移地址, 高地址:段选择子
jmp far [ds:CORE_BASE_ADDR + 0x04]
;===================================================================================
;参数: eax 段基址, esi 属性 , ebx 段界限
;返回: edx:eax 描述符 edx 高32位, eax 低32位
;合成一个描述符
;段基址:32位, 段界限:20位
make_gd:
mov edx,eax
shl eax,16 ;保留低16位,用于合成低地址的32位描述符
or ax,bx ;低32位描述符组合完成
and edx,0xffff0000 ;确保高16位基地址
rol edx,8 ;;循环左移, 把高8位移动到低8位
bswap edx ;低8位和高8位交换. 至此高32位中的段基地址处理完成
and ebx,0x000f0000 ;段界限的低16位已经在上面处理完成,只剩高4位是有效位
or edx,ebx ;段界限处理完成
or edx,esi ;合成属性
ret
;===================================================================================
;读取一个扇区
;参数 : eax 逻辑扇区号 (为了减少传参)
;默认写入到 ds:ebx
;需要用到的端口号 0x1f2 ~ 0x1f7 , 0x1f0
;端口 0x1f2 = 设置扇区数量
;LBA28有28位 扇区号
;0x1f3 ~ 0x1f6 设置逻辑扇区号, 这些端口都是8位端口,因此只能传送一个字节来满足28位扇区号
;0x1f6端口只有低4位是端口号,高前3位111表示LBA模式,后1位表示主盘(0)从盘(1)
;0x1f7 用于读写命令,以及读取硬盘状态
;一般情况下先检测0x1f7的状态,检测第7位(是否繁忙)和第3位(准备交换数据)的状态
;即检测 : 1000_1000b 与 0x1f7读取回来的状态, 如果 1000_1000b and 状态 = 0000_1000b 则可以读写
;0x1f0用于读写数据, 这是一个16位端口,一次可以读取2个字节
read_sector:
push eax
push edx
push ecx
;保存扇区号
push eax
;设置扇区数量
mov dx,0x1f2
mov al,1
out dx,al
;设置28位扇区号,端口:0x1f3 ~ 0x1f6
pop eax
inc dx ;0x1f3
out dx,al
inc dx ;0x1f4
shr eax,8
out dx,al
inc dx ;0x1f5
shr eax,8
out dx,al
inc dx ;0x1f6
shr eax,8 ;只有低4位是逻辑扇区号有效位
and al,0000_1111B ;确保高4位0
or al,0xe0 ;最低位:主盘,高3位:LBA模式 , 1110B
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读取命令
out dx,al
;读取硬盘状态
.read_disk_status:
in al,dx ;从0x1f7读取状态
and al,1000_1000b ;是否可以读取? 第3位如果是1则可以读取了
cmp al,0000_1000b ;检测第三位的状态,如果相等说明可以读取
jnz .read_disk_status ;不想等则继续等待
;开始读取扇区,从0x1f0读取数据,这是一个16位端口,一次读取2个字节
mov dx,0x1f0
mov ecx,256 ;一次读取2个字节, 256*2=512
.begin_read:
in ax,dx
mov [ds:ebx],ax ;把读取到的2个字节复制到指定位置
add ebx,2
loop .begin_read
pop ecx
pop edx
pop eax
ret
GDT_SIZE dw 0 ;GDT界限
GDT_BASE dd 0x7e00 ;GDT的起始地址
times 510-($-$$) db 0
db 0x55,0xaa
呆子内核加载与连接用户程序
- 呆子内核使用加载器创建的stack
- 对齐:
对齐的一些操作说明.如果要对4字节对齐:
能被4整除. 4的二进制:100, 最后2位都是0,
test eax,3(11B) => 如果是1则未对齐.
强制对4对齐的相关指令:
指令 | 解释 |
---|---|
mov eax,5 | - |
mov ebx,eax | 把ebx向上取整为能被4所整除的数 |
and ebx,0xfffffffc | 强制低2位都为0 |
add ebx,4 | 这个数必能被4整除, 相当于向上取整 |
test eax,11B | 如果为0则能整除,为1则无法整除 |
cmovnz eax,ebx | 条件赋值, c(条件)mov(赋值)nz(不为0则赋值,否则不会赋值) |
- 动态分配内存后栈的段基址:
-
- 由于栈描述符的E=1,一般情况下, 栈的地址空间都在段基址的下面,例如段基址:0x7c00,实际栈空间可能在0x6c00~0x7bff
-
- 下面在内存分配后,返回的是可用首地址
-
- 因此必须把首地址+栈的字节数作为段基址,否则操作栈push ,pop会覆盖不属于栈本身的地址空间
- 呆子内核在数据段提供了几个可被导出, 可以被用户程序调用的过程, 通过匹配符号信息表, 匹配成功则把过程的偏移地址,段选择子 写入到用户程序头部段的符号地址表中
;当前程序继承MBR的栈
;为了使用方便定义一些已知的段选择子.当然也可以从下面的头部间接获取
;地址对齐的一些操作:
;如果要与4字节(2^2)对齐,可以测试最低2位是否都为0,如果是则必定对齐
;如果要与512(2^9)对齐,可以测试最低9位是否都为0,如果是则必定对齐
;例如要对4字节对齐:
; mov eax,5 ; test eax,3(11B); 结果是1,没有对齐
;可以这么做:
; mov eax,5
; mov ebx,eax ;把ebx向上取整为能被4所整除的数
; and ebx,0xfffffffc ;强制低2位都为0
; add ebx,4 ;这个数必能被4整除, 相当于向上取整
; test eax,11B ;如果为0则能整除,为1则无法整除
; cmovnz eax,ebx ;条件赋值, c(条件)mov(赋值)nz(不为0则赋值,否则不会赋值)
;*关于栈的内存分配*:
;1.
;从前面MBR中栈的分配: 基址:0x7c00, 段界限:0xffffe,G=1(以4k为单位)
;实际段界限:(0xffffe+1)*4096-1+1 = 0xFFFFF000
;实际最小偏移:0x7c00 + 0xFFFFF000 = 0x(1)00006C00 由于环绕特性,最高位舍去,最终:0x6c00
;esp最大:0xffffffff, 则最大偏移: 0x7c00 + 0xffffffff = 0x(1)00007BFF,最高位舍去:0x7bff
;最小:0x6c00, 最大:0x7bff. 可以看到这块4k内存,都在基址:0x7c00下面
;可以看到 栈一般情况下,都会因为环绕特性,其实际内存位置都在基址下面
;2.
; 下面alloc_mem是一个分配内存的段间过程,方向向上,像数组一样划空间
; 如果是给方向向上的段,可以工作的很好,但如果是给栈划空间,就需要配合建立描述符的过程一起工作了
; 假设栈请求分配内存:
; 2.1 alloc_mem返回地址:0x100000, 给栈划分4k(0x1000)空间
; 2.2 下一个可用地址:0x100000+0x1000=0x101000
; 现在如果把0x100000 当成段描述的段基址,会发生错误
; 根据上面,栈的地址空间 一般(具体情况具体分析)情况都会在 段基址的下面
; alloc_mem返回的地址空间应该在:0x100000 ~ 100FFF , 共计 4096 字节,这些属于栈
; 而0x100000的下面并不属于栈,如果把0x100000当成段基址,将覆盖0x100000之前的内存数据
; 因此,段基址应该是: 0x100000 + 4k(0x1000) = 0x101000
; 这样从0x100000 ~ 0x100FFF 都是栈空间
;--------------------
;常量定义
;重定位表中的每一项段信息占用12字节
REALLOC_TABLE_EACH_ITEM_BYTES EQU 12
;符号信息表每项占用16字节
SYMBOL_TABLE_EACH_ITEM_BYTES equ 16
;用户符号信息表每项占用12字节
USER_SYMBOL_TABLE_EACH_ITEM_BYTES EQU 12
;用户程序扇区号
USER_APP_SECTOR EQU 100
;MBR中定义
SEL_4G_DATA equ 0x18 ;数据段
SEL_STACK EQU 0x20 ;栈
SEL_0XB8000 EQU 0x10 ;显存
SEL_MBR EQU 0X08 ;MBR段
;当前程序的,由MBR创建
SEL_HEADER EQU 0x28 ; 头部段选择子
SEL_CODE EQU 0x30 ;代码段
SEL_DATA EQU 0X38 ;数据段
SEL_FUNC EQU 0X40 ;函数段
;--------------------
;function 函数段内大部分都是段间过程调用
;每个过程结尾都是 retf (pop eip, pop cs)
;每个retf 后都加了立即数,恢复栈,不用自己恢复esp
;调用function段的段间过程需要 : call SEL_FUNC:过程名 => push cs, push eip
;重定位表含当前程序的4个段信息
;每个段信息包含段界限,段基址,段属性,每个属性4字节,3个属性算一项共12字节
[bits 32]
section header vstart=0 align=16
;程序长度
app_len dd tail_end ;0x00
;入口点偏移,段地址
;当此程序被加载后,物理段地址被替换成段选择子
entry dd start ;0x04
dd section.code.start ;0x08
;重定位表有几项
realloc_table_len dd (table_end - table_start)/REALLOC_TABLE_EACH_ITEM_BYTES ;0x0c
;重定位表
;重定位表中存放了每个段的 : 段基址,段界限(段长度-1),段属性
;被加载程序处理后,所有的段基址都将被替换成段选择子
table_start:
;头部段
seg_header_len dd header_end-1 ;段界限, 0x10
seg_header_addr dd section.header.start ;段基址, 0x14
seg_header_attr dd 0x00409200 ;段属性, 0x18
;代码段
seg_code_len dd code_end-1 ;段界限,0x1c
seg_code_addr dd section.code.start ;段基址,0x20
seg_code_attr dd 0x00409800 ;段属性,0x24
;数据段
seg_data_len dd data_end-1 ; 段界限,0x28
seg_data_addr dd section.data.start ;段基址,0x2c
seg_data_attr dd 0x00409200 ;段属性,0x30
;函数段
seg_function_len dd function_end-1 ;段界限,0x34
seg_function_addr dd section.function.start ;段基址,0x38
seg_function_attr dd 0x00409800 ;属性,0x3c
table_end:
;----------------
;以下段选择子由mbr传递过来
;4G数据段
seg_4g_data dd 0 ;0x40
;栈段
seg_stack dd 0 ;0x44
;显存段
seg_0xb8000 dd 0 ;0x48
;MBR段
seg_mbr dd 0 ;0x4c
header_end:
section code vstart=0 align=16
start:
;当前栈段,ss:0x20,继承使用了MBR的栈
;切换DS, 直接使用上面定义的常量,省的去头部段拿了
mov eax,SEL_DATA
mov ds,eax
;显示消息,内核启动
mov ebx,first_msg_done - first_msg ;字符串长度
push ebx
push ds
push dword first_msg ;起始地址
;call 段选择子:过程名
;具体过程:
;1.push cs, push eip
;2.根据SEL_FUNC,获取索引8 *8 + GDTR提供的GDT起始地址:
; 2.1 地址: 8*8 + 0x7e00 ,检查此地址是否越界(GDT的界限)
; 2.2 获取此地址的段描述符,加载到cs高速缓冲区
;3.查看print偏移地址是否在此段内,越界检查 (描述符的界限)
;4.mov eip, print
call SEL_FUNC:print
;add esp,0x0c . 不需要手动还原栈,段间调用都加了retf N 来恢复栈
;加载用户程序
call SEL_FUNC:load_app
;显示加载完毕信息
mov ebx,loaded_msg_done - loaded_msg ;长度
push ebx
push ds
push dword loaded_msg
call SEL_FUNC:print
;加载完成, 跳转到用户程序
;保存栈顶, 用户程序回来后恢复
mov [ds:stack_top], esp
;获取头部段选择子.进行跳转
mov eax,[ds:user_header_selector]
mov es,eax
jmp far [es:0x04]
exit_process:
;------用户程序退出后应该还需要把分配的内存收回
;------用户描述符全部删除, 修改gdt_size, 重新加载gdt,这里全部省略
;恢复自己的数据段
mov eax,SEL_DATA
mov ds,eax
;恢复栈
mov ebx,[ds:stack_top]
mov eax,SEL_STACK
mov ss,eax
mov esp,ebx
;喊一句话
mov ebx,(back_msg_done - back_msg)
push ebx
push ds
push dword back_msg
call SEL_FUNC:print
hlt
code_end:
section function vstart=0 align=16
;退出
exit:
push SEL_CODE
push exit_process
retf
;加载一个程序
load_app:
push ebp
mov ebp,esp
pushad
push es
push ds
;首先读取一个扇区,获取用户程序的头部信息
;由于需要动态给用户程序分配内存地址,因此不再直接把首个扇区读到指定内存地址
;加载用户头部的缓冲区在 自己的data段 : user_header_buffer 定义了512字节
mov eax,SEL_DATA
mov ds,eax
mov ebx,user_header_buffer
push ds ;段选择子
push ebx ;偏移
push dword USER_APP_SECTOR ;扇区号
call SEL_FUNC:read_sector
;获取程序多长
mov eax,[ds:user_header_buffer] ;长度
xor edx,edx
mov ecx,512
div ecx
;计算还需要读取几个扇区
or edx,edx ;是否有余数,有余数则+1
jz .begin_alloc_mem
inc eax ;有余数
;为用户程序分配内存地址
.begin_alloc_mem:
mov ecx,eax ;备份扇区数
;计算字节数
xor edx,edx
mov ebx,512
mul ebx ;计算字节数, eax 32位4G空间足够,不需要edx
push eax ;需要分配的字节数
call SEL_FUNC:alloc_mem ;返回eax 为可用的起始地址,用户程序将被加载到这
mov edi,eax ;保存一份起始地址
;把用户程序读取到 eax为起始地址的内存空间中
push SEL_4G_DATA ;4g 空间
push eax ;用户程序起始地址
push ecx ;扇区数量
push USER_APP_SECTOR ;起始扇区号
call SEL_FUNC:read_user_app ;读取整个用户程序
;读完用户程序,需要给程序的每个段创建描述符,才能让用户程序运行起来
push SEL_4G_DATA ;段选择子
push edi ;用户程序被加载的起始地址
call SEL_FUNC:create_user_gdt ;为用户程序创建描述符和栈空间
;把头部段选择子保存一份,后续需要用到
mov eax,SEL_4G_DATA
mov es,eax ;指向4G
mov eax,[es:edi + 0x14] ;头部段选择子
mov [ds:user_header_selector],eax
;用户程序符号表处理
;使用头部段选择子, 当然也可以使用SEL_4G_DATA + 用户程序起始地址来处理头部符号表(这样稍显麻烦)
push eax
call SEL_FUNC:realloc_user_app_symbol_table
pop ds
pop es
popad
mov esp,ebp
pop ebp
retf
;比较字符串
;参数:目标偏移,目标段选择子, 原偏移, 原段选择子 , 字符串长度
;栈中的位置:12 16 20 24 28
;返回:eax , 0:不相等, 1:相等
compare_string:
push ebp
mov ebp,esp
pushfd
push es
push ds
push esi
push edi
push ecx
push edx
mov eax,[ebp + 16] ;目标段选择子
mov es,eax
mov edi,[ebp + 12] ;目标偏移
mov eax,[ebp + 24] ;原段选择子
mov ds,eax
mov esi,[ebp + 20] ;原偏移
mov ecx,[ebp + 28] ;字符串长度
mov edx,1
xor eax,eax
cld
repe cmpsb
cmovz eax,edx ;匹配成功
.compare_string_done:
pop edx
pop ecx
pop edi
pop esi
pop ds
pop es
popfd
mov esp,ebp
pop ebp
retf 20
;符号表字符串比较
;参数:用户信息表地址偏移,用户信息表头部段选择子, 原信息表偏移, 原信息表段选择子
;栈中的位置:8 12 16 20
;返回:eax , 0:不相等, 1:相等
symbol_table_item_compare_string:
push ebp
mov ebp,esp
pushfd
push es
push ds
push edi
push esi
mov eax,[ebp + 20] ;原信息表段选择子
mov ds,eax
mov esi,[ebp + 16] ;原信息表偏移
mov eax,[ebp + 12] ;目标信息表段选择子
mov es,eax
mov edi,[ebp + 8] ;目标信息表地址偏移
xor eax,eax
;先比较字符串长度
;4字节比较. 比较esi,edi指向的字符串长度,每一项首地址都是4字节的字符串长度
push edi
push esi
cld
cmpsd ;比较后 esi+=4, edi+=4. 会自动增加
pop esi
pop edi
jnz .symbol_table_item_compare_string_done
;长度一致,进行字符串比较
push dword [es:edi] ; 首4个字节为字符串长度
push ds ;原段
push dword [ds:esi+4] ;原偏移在每项首地址的后4个字节,存放了实际字符串的地址
push es ;目标段
push dword [es:edi+4] ;目标偏移,偏移+4,存放字符串的地址
call SEL_FUNC:compare_string ;比较字符串
.symbol_table_item_compare_string_done:
pop esi
pop edi
pop ds
pop es
popfd
mov esp,ebp
pop ebp
ret 16
;比较用户符号信息表
;参数:用户地址表起始地址, 用户信息表地址, 用户头部段选择子
;栈中位置: 8 12 16
;拿用户信息表的一条与当前可以导出的信息表全部项进行比较
symbol_table_item_compare_and_fill:
push ebp
mov ebp,esp
pushad
pushfd
push es
push ds
mov eax,[ebp + 16] ;用户头部段选择子
mov es,eax
mov edi,[ebp + 12] ;用户信息表地址
mov eax,SEL_DATA
mov ds,eax ;ds指向自己数据段
mov esi,symbol_table_begin ;指向自己的信息表首项地址
mov ecx,[ds:symbol_table_len] ;自己的信息表共几项
;循环当前可导出的符号表每一项
.begin_comapre_string:
push ds ;原信息表段选择子
push esi ;原信息表偏移地址
push es ;用户头部段选择子
push edi ;用户信息表偏移(首)地址
call symbol_table_item_compare_string
or eax,eax
jnz .matched ;匹配成功,跳转
add esi,SYMBOL_TABLE_EACH_ITEM_BYTES
loop .begin_comapre_string
jmp .symbol_table_item_compare_and_fill_done ;没匹配到
;匹配成功
.matched:
push ds ;原段选择子
push esi ;原信息表偏移地址
push es ;用户段选择子
push edi ;用户信息表偏移地址
push dword [ebp + 8] ;用户地址表
call symbol_table_item_fill_addr ;填充地址
.symbol_table_item_compare_and_fill_done:
pop ds
pop es
popfd
popad
mov esp,ebp
pop ebp
ret 0x0c
;填充地址
;参数:用户地址表,用户信息表地址,用户信息表段选择子, 原信息表地址, 原段选择子
;栈中位置: 8 12 16 20 24
symbol_table_item_fill_addr:
push ebp
mov ebp , esp
pushad
push es
push ds
mov eax,[ebp + 24] ;原段选择子
mov ds,eax
mov esi,[ebp + 20] ;原信息表地址
mov eax,[ebp + 16] ;用户段选择子
mov es,eax
mov edi,[ebp + 12] ;用户信息表地址
mov edx,[ebp + 8] ;用户地址表首地址
;当前可导出的信息表结构:
; 字符串长度 , 4字节
; 字符串偏移地址, 4字节
; 字符串对应的过程地址, 4字节 -> 这个位置是要获取的, 原信息表起始地址+8
; 索引 , 4字节
mov ebx,[ds:esi + 8] ;内核数据段的地址表项,此偏移地址存放着偏移地址,段选择子
;根据用户信息表项内的索引,确定用户地址表的位置,然后进行填充偏移地址,段选择子
mov ecx,[es:edi + 8] ;用户信息表项的索引
shl ecx,3 ;乘8. 用户地址表每一项占8字节, 索引*8 => 相当于左移3位
mov eax,[ds:ebx] ;获取过程偏移地址
mov [es:edx + ecx],eax ;在用户地址表中存放偏移地址
movzx eax, word [ds:ebx + 4] ;过程的段选择子, 内核定义的地址表中段选择子是2个字节
mov [es:edx + ecx + 4], eax ;在用户地址表中存放段选择子
pop ds
pop es
popad
mov esp,ebp
pop ebp
ret 20
;用户符号表处理
;参数: 头部段选择子
realloc_user_app_symbol_table:
push ebp
mov ebp,esp
pushad
push es
mov ebx,[ebp + 12] ;头部段选择子
mov es,ebx
;比较过程:
;拿用户程序的符号信息表与自己数据段中的符号信息表中的每一项比较
;如果匹配,则把自己数据段中的符号地址表(偏移,段选择子)填充到用户的符号地址表中
;用户符号信息表起始位置 0x3c
mov ecx,[es:0x3c]
mov esi,[es:0x40] ; 用户符号信息表首项地址
mov edi,[es:0x44] ; 用户符号地址表起始地址
.compare_start:
push es ;用户头部段选择子
push esi ;用户符号信息表首项地址
push edi ;用户符号地址表起始地址
call symbol_table_item_compare_and_fill
add esi,USER_SYMBOL_TABLE_EACH_ITEM_BYTES ;指向下一个符号信息表内的起始地址
loop .compare_start
pop es
popad
mov esp,ebp
pop ebp
retf 4
;为用户程序创建描述符
;参数: 用户程序被加载的起始地址, 段选择子
create_user_gdt:
push ebp
mov ebp,esp
pushad
push es
mov eax,[ebp + 16] ;段选择子
mov es,eax
mov ebx,[ebp + 12] ;被加载的起始地址, 指向头部
mov ecx,[es:ebx + 0x0c] ;获取重定位表项数
mov esi,0x10 ;用户程序头部重定位表的起始地址
;处理重定位表
.process_realloc_table:
mov eax,[es:ebx+esi+4] ;段基址
add eax,ebx ;实际段基址
push eax ;段基址
push dword [es:ebx + esi] ;段界限
push dword [es:ebx + esi + 8] ;段属性
call SEL_FUNC:make_gd ;返回描述符, edx:eax
push edx ;高32位
push eax ;低32位
call SEL_FUNC:add_to_gdt ;加入到GDT中,返回ax(段选择子)
;把段选择子写入用户程序中
mov [es:ebx + esi + 4],eax
;指向重定位表中的下一项
add esi,USER_SYMBOL_TABLE_EACH_ITEM_BYTES
loop .process_realloc_table
;重定位表处理完毕
;把入口点的段基址替换成段选择子
mov eax,[es:ebx+0x20] ;此处在上面重定位表中已经替换完成
mov [es:ebx+0x08] , eax
;为用户程序创建栈空间
push es
push ebx
call SEL_FUNC:create_stack ;创建栈,并构建栈描述符加入到GDT中
pop es
popad
mov esp,ebp
pop ebp
retf 8
;创建栈
;参数:用户程序起始地址, 段选择子
create_stack:
push ebp
mov ebp, esp
pushad
push es
mov eax,[ebp + 16] ;段选择子
mov es,eax
mov ebx,[ebp + 12] ;被加载的起始地址, 指向头部
;ecx:指定栈的界限
mov ecx,0xfffff ;最大界限
mov eax,[es:ebx + 0x34] ;获取栈指定的大小
sub ecx,eax ;减去栈指定的长度,这样就得到了栈的最小偏移
;edx:指定栈的属性
mov edx,0x00c09600 ;G=1,E=1,方向往下的数据段
;下面调用alloc_mem 来分配一个段基址
;首先确定需要分配多大的空间, 根据上面的eax获取到的以4K为单位的数值*4096即可
;4096=(2^12).因此左移12次
shl eax,12
mov esi,eax ;备份,等地址返回后需要用到
push eax
call SEL_FUNC:alloc_mem ;返回eax, 可用地址
;把基址提高N字节,此地址相当于下一个alloc_mem分配的内存地址
;把此地址当成段基址
add eax,esi ;由于地址环绕特性,如果不这样做会覆盖掉前面的内存数据,具体在上面已经写过了
;为栈创建描述符
push eax ;段基址
push ecx ;段界限
push edx ;段属性
call SEL_FUNC:make_gd ;返回edx:eax
push edx
push eax
call SEL_FUNC:add_to_gdt ;增加到GDT中,返回eax 段选择子
;把段选择子写回用户程序
mov [es:ebx + 0x38], eax
pop es
popad
mov esp,ebp
pop ebp
retf 8
;把8字节的描述符加入到GDT中
;参数:低32位,高32位
;返回eax, (ax有效位)段选择子
add_to_gdt:
push ebp
mov ebp,esp
push es
push ds
push esi
push ebx
mov esi,SEL_DATA
mov ds,esi ;指向自己的数据段,获取用于获取 GDT_SIZE ,GDT_BASE
mov esi,SEL_4G_DATA
mov es,esi ;指向4G,用于增加描述符
sgdt [ds:gdt_size] ;存放GDTR所存的值,6字节
xor esi,esi
xor ebx,ebx
mov bx, word [ds:gdt_size] ;获取GDT界限
inc bx ;指向下一个可存放的偏移地址
mov esi,[ds:gdt_base] ;GDT起始地址
add esi,ebx ;可存放描述符的地址
;增加描述符到GDT
mov ebx,[ebp + 12] ;低32位描述符
mov [es:esi],ebx
mov ebx,[ebp + 16] ;高32位
mov [es:esi + 4],ebx
add word [ds:gdt_size],8 ;增加界限
lgdt [ds:gdt_size] ;重载GDT, 使之生效
;构造描述符的段选择子
;获取界限, 除以8(右移3位) , 得到索引
xor eax,eax
mov ax,[ds:gdt_size]
shr ax,3 ;得到索引
;由于当前是在GDT中,TI位置0,DPL忽略,因此左移3位即可
shl ax,3
pop ebx
pop esi
pop ds
pop es
mov esp,ebp
pop ebp
retf 8
;根据:段基址,段界限,段属性创建一个描述符
;参数: 段属性, 段界限, 段基址
;返回: edx:eax 一个8字节描述符,edx高32位,eax低32位
make_gd:
push ebp
mov ebp,esp
push ecx
push ebx
mov eax,[ebp + 20] ;段基址
mov ebx,[ebp + 16] ;段界限(低20位有效位)
mov ecx,[ebp + 12] ;段属性
mov edx,eax
shl eax,16 ;保留低16位段基址
or ax,bx ;eax 描述符低32位合成完毕
and edx,0xffff0000 ;保留高16位
rol edx,8 ;循环左移,把高8位移动到低8位
bswap edx ;交换低地址和高地址. 段基址位置存放完毕
and ebx,0x000f0000 ;段界限保留高4位
or edx,ebx ;段界限合成完
or edx,ecx ;组合属性
pop ebx
pop ecx
mov esp,ebp
pop ebp
retf 0x0c
;读取整个用户程序
;参数: 起始扇区号,扇区数量,程序被加载的起始地址,目标内存段选择子(注:此选择子一般情况是SEL_4G_DATA)
read_user_app:
push ebp
mov ebp,esp
pushad
push es
mov ecx,[ebp + 16] ;扇区数量
mov edx,[ebp + 12] ;起始扇区号
mov ebx,[ebp + 20] ;程序被加载的起始地址 , 是内存分配出来的地址
mov eax,[ebp + 24] ;段选择子
mov es,eax ;一般情况是SEL_4G_DATA. 指向4G空间
.read_one_sector:
push es ;段选择子
push ebx ;偏移, ebx在read_sector中自增
push edx ;扇区号
call SEL_FUNC:read_sector
inc edx ;读取下一个扇区
loop .read_one_sector
pop es
popad
mov esp,ebp
pop ebp
retf 0x10
;分配一块内存
;参数: 需要多少字节
;返回: eax :一个以4字节对齐的内存起始地址
alloc_mem:
push ebp
mov ebp,esp
push ds
push ebx
push ecx
mov eax,[ebp + 12] ;参数
mov ebx,SEL_DATA
mov ds,ebx
mov ebx,[ds:user_mem_base_addr] ;获取可用的内存地址, ebx 此地址用于返回
add eax,ebx ;计算下一个可用地址
;让下一个内存地址是4字节对齐的, 也就是能被4整除的,即低2位都是0
mov ecx,eax ;确保ecx一定能被4整除
and ecx,0xfffffffc ;强制低2位都是0
add ecx,4 ;这个数一定能被4整除
;看看eax是否能被4整除, and eax,3, 如果不为0说明无法被4整除
test eax,3 ;3的二进制:11
cmovnz eax,ecx ;不为0,则把能整除的ecx赋值给eax
;为下一个可用地址赋值
mov [ds:user_mem_base_addr],eax
xchg eax,ebx
pop ecx
pop ebx
pop ds
mov esp,ebp
pop ebp
retf 4
;读取扇区
;参数: 扇区号,目标偏移地址, 目标段选择子
;返回 ebx 以一个可写入的地址
;端口: 0x1f2 用来设置扇区数量
;0x1f3 - 0x1f6 用来设置扇区号, 其中0x1f6只有低4位是最后的扇区号
;0x1f7 用来控制读/写, 反馈状态
;0x1f0 用来读取数据
;28位扇区号,LBA模式
read_sector:
push ebp
mov ebp,esp
push eax
push es
push edx
push ecx
mov eax,[ebp + 20] ;段选择子
mov es,eax
mov ebx,[ebp + 16] ;偏移地址
mov eax,[ebp + 12] ;扇区号
push eax
mov dx,0x1f2 ;设置扇区数
mov al,1
out dx,al
inc dx ;0x1f3
pop eax
out dx,al
inc dx ;0x1f4
shr eax,8 ;去掉低8位
out dx,al
inc dx ;0x1f5
shr eax,8
out dx,al
inc dx ;0x1f6
shr eax,8
and al,0000_1111B ;保留低4位有效位
or al,0xe0 ;1110_000B , LBA模式,从主盘读
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读取状态
out dx,al
;查看硬盘状态
.read_disk_status:
in al,dx
and al,1000_1000b ;检查是否可读
cmp al,0000_1000b ;如果第三位为1,则可读
jnz .read_disk_status ;否则继续检查
;从0x1f0中开始读取
mov dx,0x1f0
mov ecx,256
.begin_read:
in ax,dx
mov [es:ebx],ax
add ebx,2
loop .begin_read
pop ecx
pop edx
pop es
pop eax
mov esp,ebp
pop ebp
retf 0x0c
;打印到显存
;参数:偏移地址,段选择子,字符串长度
print:
push ebp
mov ebp,esp
push ds
push es
push esi
push edi
push ecx
push eax
mov esi,[ebp + 12] ;偏移
mov ecx,[ebp + 16] ;段选择子
mov ds,ecx
mov ecx,[ebp + 20] ;长度
;获取最后一次的打印位置
mov eax,SEL_DATA
mov es,eax
xor edi,edi
mov di,[es:print_pos] ;从此偏移开始打印
;设置显存段
mov eax,SEL_0XB8000
mov es,eax
.print_loop:
mov al,[ds:esi]
mov [es:edi],al
inc di
mov byte [es:edi],0x07
inc esi
inc di
loop .print_loop
.print_done:
mov eax,SEL_DATA
mov es,eax
mov [es:print_pos],edi
pop eax
pop ecx
pop edi
pop esi
pop es
pop ds
mov esp,ebp
pop ebp
retf 0x0c ;pop eip , pop cs
function_end:
section data vstart=0 align=16
first_msg db 'fuck !'
first_msg_done:
loaded_msg db 'loaded!!!'
loaded_msg_done:
back_msg db 'Im back!!!!!!!'
back_msg_done:
;栈顶,备份. 为了跳转回来后还原
stack_top dd 0
;print_pos : 当前打印的位置
print_pos dw 0
;由于加载用户程序后会添加描述符,需要更改gdt_size
;sgdt 指令用于存放gdt数据
gdt_size dw 0 ;界限
gdt_base dd 0 ;GDT起始地址
;用户程序被加载到的地址,1M开始处
user_mem_base_addr dd 0x100000
;用户头部段选择子,用于后续跳转
user_header_selector dd 0
;用于读取用户程序首个扇区
user_header_buffer times 512 db 0
;符号信息表的项数
symbol_table_len dd (symbol_table_end - symbol_table_begin) / SYMBOL_TABLE_EACH_ITEM_BYTES
;可导出可被连接(用户程序可见可用)的符号信息表
symbol_table_begin:
print_info:
dd (read_sector_string - print_string) ;字符串长度(用于比较)
dd print_string ;字符串的偏移(用于匹配)
dd print_addr ;过程地址
dd 0 ;索引 (这里没用到)
read_sector_info:
dd (make_gd_string - read_sector_string)
dd read_sector_string
dd read_sector_addr
dd 1
make_gd_info:
dd (exit_string - make_gd_string)
dd make_gd_string
dd make_gd_addr
dd 2
exit_info:
dd (compare_string_string - exit_string)
dd exit_string
dd exit_addr
dd 3
compare_string_info:
dd (symbol_table_string_end - compare_string_string)
dd compare_string_string
dd compare_string_addr
dd 4
symbol_table_end:
;字符串表
symbol_table_string_begin:
print_string db 'print'
read_sector_string db 'read_sector'
make_gd_string db 'make_gd'
exit_string db 'exit'
compare_string_string db 'compare_string'
symbol_table_string_end:
;地址表
symbol_table_addr_begin:
print_addr dd print ;偏移
dw SEL_FUNC ;段选择子
read_sector_addr dd read_sector
dw SEL_FUNC
make_gd_addr dd make_gd
dw SEL_FUNC
exit_addr dd exit
dw SEL_FUNC
compare_string_addr dd compare_string
dw SEL_FUNC
symbol_table_addr_end:
data_end:
section tail
tail_end:
用户程序
- 用户程序头部段定了重定位表和符号表
- 符号表分为2个大部分:符号信息表和符号地址表
- 内核会对比信息表中的数据, 去匹配自己可导出的过程的名字,一旦匹配成功,会把对应的偏移,段选择子填充到用户地址表中, 这样用户就能调用提供的一些过程
;定义常量
;每一项重定位段信息占用12字节
USER_REALLOC_TABLE_EACH_ITEM_BYTES EQU 12
;符号表每一项占用12字节
USER_APP_EACH_TABLE_ITEM_BYTES EQU 12
;符号地址表每一项占用8字节
USER_APP_EACH_TABLE_ITEM_ADDR_BYTES equ 8
;栈段由内核分配,并写入选择子到seg_stack_addr;
;seg_stack_len 提示描述符的G位(以4K还是1字节为单位)
;用户程序如果需要调用内核程序提供的API,则需要符号表帮忙,根据字符串匹配来确定对应的段选择子:偏移地址
;符号表分为2块:
;1.符号信息表
; 1.符号信息表内部又由2部分组成: 符号信息,符号字符串表
; 2.遍历符号信息,可匹配字符串以及获取对应的索引,一旦匹配成功则在地址表中写入(偏移,段选择子)
;2.符号地址表,每一项占8个字节: (4字节)偏移地址:(4字节,仅低2字节有效)段选择子
; 根据信息表内的索引, 索引*8 即可获取过程的偏移,段选择子
[bits 32]
section header vstart=0 align=16
;程序长度
app_len dd tail_end ;0x00
;入口点
entry dd start ;0x04
dd section.code.start ;0x08
;重定位表项数
;0x0c
realloc_table_len dd (table_end - table_begin) / USER_REALLOC_TABLE_EACH_ITEM_BYTES
;重定位表
;被加载后,所有段基址都会被替换成段选择子
table_begin:
;头部段
seg_header_len dd header_end-1 ;段界限, 0x10
seg_header_addr dd section.header.start ;段基址, 0x14
seg_header_attr dd 0x00409200 ;段属性,0x18
;代码段
seg_code_len dd code_end-1 ;段界限,0x1c
seg_code_addr dd section.code.start ;段基址,0x20
seg_code_attr dd 0x00409800 ;段属性,0x24
;数据段
seg_data_len dd data_end-1 ;段界限,0x28
seg_data_addr dd section.data.start ;段基址,0x2c
seg_data_attr dd 0x00409200 ;段属性,0x30
table_end:
;栈段由内核程序帮忙分配
;分配完后由内核程序写入
;用户程序在固定位置有占位即可
;内核将用最大界限值:0xfffff - 1(这里的数字), 来计算分配多少字节
seg_stack_len dd 1 ; 0x34 , 以4K为单位, 这里的1相当于4096字节,与界限相呼应
seg_stack_addr dd 0 ; 0x38, 由内核写入栈段选择子
;0x3c
;符号信息表长度
symbol_table_len dd (symbol_table_string_start-symbol_table_begin) / USER_APP_EACH_TABLE_ITEM_BYTES
;0x40
;符号信息表起始位置
symbol_table_info_start dd symbol_table_begin
;0x44
;符号地址表起始位置
symbol_table_addr_start dd symbol_table_addr_begin
;用户程序符号信息表
symbol_table_begin:
print_info:
dd (exit - print ) ;字符串长度,用于比较
dd print ;字符串的偏移地址
dd 0 ;对应地址表的索引
exit_info:
dd (symbol_table_string_end - exit)
dd exit ;字符串偏移地址
dd 1 ;索引
symbol_table_string_start:
print db 'print'
exit db 'exit'
symbol_table_string_end:
symbol_table_end:
;用户程序符号地址表
;times N db 0 占位
;每个地址:(低4字节)偏移,(高4字节)段选择子. 段选择子只有低2字节有效
;原本可以把段选择子使用2字节,选择4字节只是为了计算索引方便
;匹配成功后,会把 print , exit 的(偏移地址,段选择子)放入此表内
;根据print,exit对应的索引,放入指定位置
;由于每个地址占8个字节,根据: 索引*8 即可得到其地址
; 乘以8 , 相当于 左移3位
;例如 exit 的索引是1.
; 1. mov eax, 1 ; 索引
; 2. shl eax, 3; 乘8
; 就可以得到 exit 的偏移和段选择子
symbol_table_addr_begin:
times ((symbol_table_string_start-symbol_table_begin)/USER_APP_EACH_TABLE_ITEM_BYTES)*USER_APP_EACH_TABLE_ITEM_ADDR_BYTES db 0
symbol_table_addr_end:
header_end:
section code vstart=0 align=16
start:
;用户程序开始执行
;从内核跳转过来, es段指向头部
;切换栈段
mov eax,[es:seg_stack_addr]
mov ss,eax
xor esp,esp
;切换数据段
mov eax,[es:seg_data_addr]
mov ds,eax
;显示用户程序自己的消息
;print 偏移地址,段选择子 在 符号地址表:0
push dword (user_msg_end - user_msg)
push ds
push dword user_msg
call far [es:symbol_table_addr_begin + 0]
;跳转 exit,将回到core中
;exit的索引为1, 1 * 8 为其偏移位置
jmp far [es:symbol_table_addr_begin + 8]
code_end:
section data vstart=0 align=16
user_msg db 'user is running~~'
user_msg_end:
data_end:
section tail
tail_end: