# 加载用户程序

Part 1、TCB, Task Control Block, 任务控制块

分配内存作为该任务的TCB,并插入至TCB链表。

Part 2、LDT, Locak Descriptor, 局部描述符表

分配内存作为该任务的LDT。

为用户程序的各个段构建段描述符,并写入LDT

将LDT写入GDT,得到对应的选择子。

将LDT的相关信息写入TCBTSS

Part 3、TSS, Task State Segment, 任务状态段

分配内存作为该任务的TSS。

将TSS写入GDT,得到对应的选择子。

将TSS的相关信息写入TCB

Part 4、用户程序。

分配内存用于加载用户程序。

将用户程序的大小和起始内存地址登记到TCB中。

从硬盘加载用户程序到内存。

重定位用户程序所调用的系统API,回填对应的入口地址至用户程序头部。

为用户程序的各个段构建段描述符,并写入LDT。

为该任务创建额外特权级的栈,并各个栈的相关信息写入TCBTSS。通过调用门的控制转移通常会改变当前特权级CPL,同时还要切换到与目标代码段特权级相同的栈。

# 安装调用门(call-gate)

安装整个系统服务的调用门,特权级之间的控制转移必须使用门。

1、为每个系统api构建调用门描述符,并写入GDT,同时得到对应的选择子

2、将每个系统api对应的调用门选择子写回至该api

通过调用门调用系统api格式:call far 调用门选择子

(处理器根据描述符可识别出该选择子是调用门描述符还是普通的段描述符)

    ; 安装整个系统服务的调用门。特权级之间的控制转移必须使用门mov edi, sys_apimov ecx, sys_api_items.make_call_gate:push ecxmov eax, [edi+256]          ; 该sys_api入口点的32位偏移地址mov bx, [edi+260]           ; 该sys_api所在代码段的选择子mov cx, 1_11_0_1100_000_00000B  ; 调用门属性:P=1 DPL=3 参数数量=0; 3以上的特权级才允许访问call sel_sys_routine_seg:make_gate_descriptor   ; 创建调用门描述符call sel_sys_routine_seg:setup_gdt_descriptor   ; 将调用门描述符写入gdtmov [edi+260], cx               ; 将门描述符的选择子(即调用门选择子)写回add edi, sys_api_item_length    ; 指向下一个sys_api条目pop ecxloop .make_call_gate; 测试一下刚安装好的调用门; 显示字符串mov ebx, message_callgate_mount_succcall far [sys_api_1+256]        ; 取得32位偏移地址 和16位的段选择子; 处理器会检查选择子是调用门的描述符还是普通的段描述符; 不通过调用门,以传统方式调用系统api; 显示提示信息,开始加载用户程序mov ebx, message_app_load_begincall sel_sys_routine_seg:show_string

# 困惑 ??????

VirtualBox运行结果正常,但Bochs调试会报错重启!!!

在c14_core.asm中的加载完用户程序(load_relocate_program),并成功显示“Done.”字符串后,报错!!!

    ; 加载并重定位用户程序; 通过栈传入参数push dword app_lba_begin    ; 用户程序在硬盘中逻辑扇区号push ecx                    ; 用户程序的任务控制块TCB地址call load_relocate_program  ; call指令相对近调用时自动执行push eip; 显示提示信息,用户程序加载完成mov ebx, message_app_load_succcall sel_sys_routine_seg:show_string

报错信息:

00017444510e[CPU0  ] read_virtual_checks(): read beyond limit
00017444510e[CPU0  ] interrupt(): gate descriptor is not valid sys seg (vector=0x0d)
00017444510e[CPU0  ] interrupt(): gate descriptor is not valid sys seg (vector=0x08)......(0).[17444510] [0x00000004130f] 0028:0000000000000083 (unk. ctxt): rep movsd dword ptr es:[edi], dword ptr ds:[esi] ; f3a5
00017444510e[CPU0  ] exception(): 3rd (13) exception with no resolution, shutdown status is 00h, resetting
00017444510i[SYS   ] bx_pc_system_c::Reset(HARDWARE) called
00017444510i[CPU0  ] cpu hardware reset

# 执行结果

备注:需将用户程序代码(c14.asm)中,返回至内核“ jmp far [fs:TerminateProgram]” 改为call far

# file_02: c14_core.asm

; FILE: c13_core.asm
; DATE: 20200104
; TITLE: mini内核; 常量
; 伪指令equ仅仅是允许用符号代替具体的数值,但声明的数值并不占用空间
; 这些选择子对应的gdt描述符会在mbr中的内核初始化阶段创建
; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)
sel_core_code_seg   equ 0x38    ; gdt第7号描述符,内核代码段选择子
sel_core_data_seg   equ 0x30    ; gdt第6号描述符,内核数据段选择子
sel_sys_routine_seg equ 0x28    ; gdt第5号描述符,系统API代码段的选择子
sel_video_ram_seg   equ 0x20    ; gdt第4号描述符,视频显示缓冲区的段选择子
sel_core_stack_seg  equ 0x18    ; gdt第3号描述符,内核堆栈段选择子
sel_mem_0_4gb_seg   equ 0x08    ; gdt第1号描述符,整个0~4GB内存的段选择子app_lba_begin       equ 50      ; 将配套的的用户程序从磁盘lba逻辑扇区50开始写入; ===============================================================================
SECTION head vstart=0               ; mini内核的头部,用于mbr加载mini内核core_length         dd core_end                     ; mini内核总长度, 0x00segment_sys_routine dd section.sys_routine.start    ; 系统API代码段起始汇编地址,0x04
sys_routine_length  dd sys_routine_end              ; 0x08segment_core_data   dd section.core_data.start      ; mini内核数据段起始汇编地址,0x0c
core_data_length    dd core_data_end                ; 0x10segment_core_code   dd section.core_code.start      ; mini内核代码段起始汇编地址,0x14
core_code_length    dd core_code_end                ; 0x18core_entry          dd beginning                    ; mini内核入口点(32位的段内偏移地址),0x1cdw sel_core_code_seg            ; 16位的段选择子; ===============================================================================
[bits 32]; ===============================================================================
SECTION core_code vstart=0               ; mini内核代码
beginning:mov ecx, sel_core_data_segmov ds, ecx                 ; 使ds指向mini内核数据段; 显示提示信息,内核已加载成功并开始执行mov ebx, message_kernel_load_succcall sel_sys_routine_seg:show_string ; 调用系统api,显示一段文字; call 段选择子:段内偏移; 获取处理器品牌信息mov eax, 0          ; 先用0号功能探测处理器最大能支持的功能号cpuid               ; 会在eax中返回最大可支持的功能号; 要返回处理器品牌信息,需使用0x80000002~0x80000004号功能,分3次进行mov eax, 0x80000002cpuidmov [cpu_brand], eaxmov [cpu_brand+0x04], ebxmov [cpu_brand+0x08], ecxmov [cpu_brand+0x0c], edxmov eax, 0x80000003cpuidmov [cpu_brand+0x10], eaxmov [cpu_brand+0x14], ebxmov [cpu_brand+0x18], ecxmov [cpu_brand+0x1c], edxmov eax, 0x80000004cpuidmov [cpu_brand+0x20], eaxmov [cpu_brand+0x24], ebxmov [cpu_brand+0x28], ecxmov [cpu_brand+0x2c], edx; 显示处理器品牌信息mov ebx, cpu_brand0         ; 空行call sel_sys_routine_seg:show_stringmov ebx, cpu_brand          ; 处理器品牌信息call sel_sys_routine_seg:show_stringmov ebx, cpu_brand1         ; 空行call sel_sys_routine_seg:show_string; 安装整个系统服务的调用门。特权级之间的控制转移必须使用门mov edi, sys_apimov ecx, sys_api_items.make_call_gate:push ecxmov eax, [edi+256]          ; 该sys_api入口点的32位偏移地址mov bx, [edi+260]           ; 该sys_api所在代码段的选择子mov cx, 1_11_0_1100_000_00000B  ; 调用门属性:P=1 DPL=3 参数数量=0; 3以上的特权级才允许访问call sel_sys_routine_seg:make_gate_descriptor   ; 创建调用门描述符call sel_sys_routine_seg:setup_gdt_descriptor   ; 将调用门描述符写入gdtmov [edi+260], cx               ; 将门描述符的选择子(即调用门选择子)写回add edi, sys_api_item_length    ; 指向下一个sys_api条目pop ecxloop .make_call_gate; 测试一下刚安装好的调用门; 显示字符串mov ebx, message_callgate_mount_succcall far [sys_api_1+256]        ; 取得32位偏移地址 和16位的段选择子; 处理器会检查选择子是调用门的描述符还是普通的段描述符; 不通过调用门,以传统方式调用系统api; 显示提示信息,开始加载用户程序mov ebx, message_app_load_begincall sel_sys_routine_seg:show_string; 这里自定义的TCB结构需要0x46字节的内存空间mov ecx, 0x46call sel_sys_routine_seg:allocate_memorycall append_to_tcb_link; 加载并重定位用户程序; 通过栈传入参数push dword app_lba_begin    ; 用户程序在硬盘中逻辑扇区号push ecx                    ; 用户程序的任务控制块TCB地址call load_relocate_program  ; call指令相对近调用时自动执行push eip; 显示提示信息,用户程序加载完成mov ebx, message_app_load_succcall sel_sys_routine_seg:show_string; 将控制转移到用户程序; 即,从0特权级转到3特权级,从0特权级全局空间转移到3特权级局部空间执行; 通常情况,这既不允许,也不太可能; 假装从调用门返回; 先确立身份,使TR和LDTR寄存器指向这个任务,然后假装从调用门返回mov eax, sel_mem_0_4gb_segmov ds, eaxltr [ecx+0x18]      ; load task register,TR指向TSS。加载任务状态段lldt [ecx+0x10]     ; load local descriptor table,LDTR指向LDT。加载LDT; 这里ecx是前面调用allocate_memory的返回值mov eax, [ecx+0x44]mov ds, eax         ; 切换到用户程序头部段; 局部描述符表LDT已经生效,可以通过它访问用户程序的私有内存段了; 此处该选择子RPL请求特权级为3,TI位为1即指向任务自己的LDT; 模仿处理器压入返回参数,假装从调用门返回push dword [0x08]   ; 从用户程序头部取出堆栈段选择子sspush dword 0        ; 栈指针esppush dword [0x14]   ; 代码段选择子cspush dword [0x10]   ; 指令指针eipretf                ; 假装从调用门返回; 于是控制转移到用户程序的3特权级代码开始执行; mov [kernel_esp_pointer], esp   ; 临时保存内核的堆栈指针; 进入用户程序后,会切换到用户的堆栈; 从用户程序返回时,可通过这里还原内核栈指针; mov ds, ax      ; 使ds指向用户程序头部段; 此处的ax值是load_relocate_program的返回值; jmp far [0x10]  ; 跳转到用户程序执行,控制权交给用户程序; 0x10, 应用程序的头部包含了用户程序的入口点; Function: 加载并重定位用户程序
; Input: PUSH app起始逻辑扇区号; PUSH app任务控制块TCB线性地址
load_relocate_program:; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDIpushadpush dspush esmov ebp, esp                        ; 栈基址寄存器mov ecx, sel_mem_0_4gb_segmov es, ecx                         ; 切换es到0~4GB的段mov esi, [ebp+11*4]                 ; 从堆栈中取得用户程序的TCB基地址; 申请创建LDT所需的内存mov ecx, 160                        ; 160字节,允许安装20个LDT描述符call sel_sys_routine_seg:allocate_memorymov [es:esi+0x0c], ecx              ; 登记LDT基地址到TCB中mov word [es:esi+0x0a], 0xffff      ; 登记LDT界限值到TCB中; 和GDT一样,LDT的界限值等于总字节数减1。初始时,0-1=0xFFFFD; 开始加载用户程序; 先读取一个扇区mov eax, sel_core_data_seg          ; 切换ds到内核数据段mov ds, eaxmov eax, [ebp+12*4]                 ; 从堆栈中取得用户程序所在硬盘的起始逻辑扇区号 mov ebx, core_buf                   ; 自定义的一段内核缓冲区; 在内核中开辟出一段固定的空间,有便于分析、加工和中转数据call sel_sys_routine_seg:read_hard_disk_0 ; 先读一个扇区; 包含了头部信息:程序大小、入口点、段重定位表; 判断需要加载的整个程序有多大mov eax, [core_buf]             ; 0x00, 应用程序的头部包含了程序大小mov ebx, eaxand ebx, 0xfffffe00             ; 能被512整除的数,其低9位都为0; 将低9位清零,等于是去掉那些不足512字节的零头add ebx, 512                    ; 加上512,等于是将那些零头凑整test eax, 0x000001ff            ; 判断程序大小是否恰好为512的倍数cmovnz eax, ebx                 ; 条件传送指令,nz 不为零则传送; 为零,则不传送,依然采用用户程序原本的长度值eaxmov ecx, eax                    ; 需要申请的内存大小call sel_sys_routine_seg:allocate_memorymov [es:esi+0x06], ecx          ; 登记用户程序加载到内存的基地址到TCB中mov ebx, ecx                    ; 申请到的内存首地址; 作为起始地址,从硬盘上加载整个用户程序; push ebx                        ; 用于后面访问用户程序头部; 从硬盘上加载整个用户程序到已分配的物理内存中xor edx, edxmov ecx, 512div ecx                         ; 用户程序占硬盘的逻辑扇区个数mov ecx, eax                    ; 循环读取的次数mov eax, sel_mem_0_4gb_segmov ds, eax                     ; 切换ds到0~4GB的段mov eax, [ebp+12*4]             ; 起始扇区号.loop_read_hard_disk:call sel_sys_routine_seg:read_hard_disk_0; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址inc eaxadd ebx, 512loop .loop_read_hard_disk       ; 循环读    ; 根据头部信息创建段描述符; pop edi                         ; 弹出ebx,恢复程序装载的首地址mov edi, [es:esi+0x06]          ; 从用户程序的TCB中取得程序装载的首地址; 创建ldt第#0号描述符; 建立用户程序头部段描述符mov eax, edi                    ; 基地址mov ebx, [edi+0x04]             ; 0x04, 应用程序的头部包含了用户程序头部段的长度dec ebx                         ; 粒度为字节的段,段界限在数值上等于段长度减1mov ecx, 0x0040_f200            ; 字节粒度的数据段属性值(无关位则置0); DPL 为3,即最低的特权级call sel_sys_routine_seg:make_gdt_descriptor    ; 构建段描述符mov ebx, esi                    ; 用户程序的任务控制块TCB地址call setup_ldt_descriptor       ; 写入ldtor cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3mov [es:esi+0x44], cx           ; 登记该段的段选择子到TCB中    mov [edi+0x04], cx              ; 0x04, 将该段的段选择子写回到用户程序头部; 创建ldt第#1号描述符; 建立用户程序代码段描述符mov eax, ediadd eax, [edi+0x18]             ; 0x18, 应用程序的头部包含了用户程序代码段的起始汇编地址; 内核加载用户程序的首地址,加上代码段的起始汇编地址,得到代码段在物理内存中的基地址mov ebx, [edi+0x1c]             ; 0x1c, 应用程序的头部包含了用户程序代码段长度dec ebx                         ; 段界限mov ecx, 0x0040_f800            ; 字节粒度的代码段属性值(无关位则置0); DPL 为3,即最低的特权级call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                    ; 用户程序的任务控制块TCB地址call setup_ldt_descriptor       ; 写入ldt    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3    ; mov [edi+0x18], cx              ; 0x18, 将该段的段选择子写回到用户程序头部mov [edi+0x14], cx              ; 0x14, 将该段的段选择子写回到用户程序头部; 应用程序头部中,和0x10处的双字一起,共同组成一个6字节的入口点,内核从这里转移控制给用户程序; 创建ldt第#2号描述符; 建立用户程序数据段描述符mov eax, ediadd eax, [edi+0x20]             ; 0x20, 应用程序的头部包含了用户程序数据段的起始汇编地址; 内核加载用户程序的首地址,加上数据段的起始汇编地址,得到数据段在物理内存中的基地址mov ebx, [edi+0x24]             ; 0x24, 应用程序的头部包含了用户程序数据段长度dec ebx                         ; 段界限mov ecx, 0x0040_f200            ; 字节粒度的数据段属性值(无关位则置0); DPL 为3,即最低的特权级call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                    ; 用户程序的任务控制块TCB地址call setup_ldt_descriptor       ; 写入ldt    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3      mov [edi+0x20], cx     ; 创建ldt第#3号描述符; 建立用户程序堆栈段描述符mov ecx, [edi+0x0c]             ; 0x0c, 应用程序的头部包含了用户程序栈段大小,以4KB为单位; 计算栈段的界限; 粒度为4KB,栈段界限值=0xFFFFF - 栈段大小(4KB个数), 例如 0xFFFFF-2=0xFFFFD; 当处理器访问该栈段时,实际使用的段界限为 (0xFFFFD+1)*0x1000 - 1 = 0xFFFFDFFF; 即,ESP的值只允许在0xFFFF DFFF和0xFFFF FFFF之间变化,共8KB; 4KB, 即2^12=0x1000; 4GB, 即2^32; 4GB/4KB=2^20=0x10_0000, 段界限=段长-1=0xF_FFFFmov ebx, 0x000f_ffffsub ebx, ecx                    ; 段界限mov eax, 0x0000_1000            ; 粒度为4KB; 32位eax乘另一个32位,结果为edx:eaxmul dword [edi+0x0c]            ; 栈大小mov ecx, eax                    ; 准备为堆栈分配内存, eax为上面乘的结果,即栈大小call sel_sys_routine_seg:allocate_memoryadd eax, ecx                    ; 和数据段不同,栈描述符的基地址是栈空间的高端地址mov ecx, 0x00c0_f600            ; 4KB粒度的堆栈段属性值(无关位则置0); DPL 为3,即最低的特权级call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                    ; 用户程序的任务控制块TCB地址call setup_ldt_descriptor       ; 写入ldt    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3 mov [edi+0x08], cx              ; 0x08, 写回到应用程序的头部; 重定位用户程序所调用的系统API; 回填它们对应的入口地址; 内外循环:外循环依次取出用户程序需调用的系统api,内循环遍历内核所有的系统api找到用户需调用那个mov eax, sel_mem_0_4gb_seg      ; 头部段描述符已安装,但还没有生效,故只能通过4GB内存段访问用户程序头部mov es, eax                     mov eax, sel_core_data_segmov ds, eax                     ; 使ds指向mini内核数据段cld     ; 清标志寄存器EFLAGS中的方向标志位,使cmps指令正向比较mov ecx, [es:edi+0x28]          ; 0x28, 应用程序的头部包含了所需调用系统API个数; edi 前面已将其赋值为用户程序的起始装载地址; 外循环次数add edi, 0x2c                   ; 0x2c, 应用程序头部中调用系统api列表的起始偏移地址.search_sys_api_external:push ecxpush edimov ecx, sys_api_items          ; 内循环次数mov esi, sys_api                ; 内核中系统api列表的起始偏移地址.search_sys_api_internal:push esipush edipush ecxmov ecx, 64             ; 检索表中,每一条的比较次数; 每一项256字节,每次比较4字节,故64次repe cmpsd              ; cmpsd每次比较4字节,repe如果相同则继续jnz .b4                 ; ZF=1, 即结果为0,表示比较结果为相同,ZF=0, 即结果为1,不同; 不同,则开始下一条目的比较; 将系统api的入口地址写回到用户程序头部中对应api条目的开始6字节mov eax, [esi]          ; 匹配成功时,esi指向每个条目后的入口地址mov [es:edi-256], eax   ; 回填入口地址mov ax, [esi+4]         ; 对应的段选择子or ax, 0000_0000_0000_0011B     ; 在创建这些调用门时,选择子的RPL为0。即,这些调用门选择子的请求特权级为0mov [es:edi-252], ax            ; 回填调用门选择子.b4:pop ecxpop edipop esiadd esi, sys_api_item_length    ; 内核中系统api列表的下一条目的偏移地址loop .search_sys_api_internalpop edipop ecxadd edi, 256                    ; 应用程序头部中调用系统api列表的下一条目的偏移地址loop .search_sys_api_external; 创建0 1 2特权级的栈; 通过调用门的控制转移通常会改变当前特权级CPL,同时还要切换到与目标代码段特权级相同的栈。; 为此,必须为每个任务定义额外的栈。; 这些额外的栈需要登记在任务状态段TSS中,以便处理器能够自动访问到。; 但目前还没有创建TSS,所以先将这些栈信息登记在任务控制块TCB中暂存mov esi, [ebp+11*4]             ; 从堆栈中取得用户程序的TCB基地址; 创建0特权级堆栈mov ecx, 0x1000             ; 申请创建0特权级堆栈所需的4KB内存mov eax, ecx                ; 用于后面生成堆栈顶地址(即栈基址)mov [es:esi+0x1a], ecx      ; 登记0特权级堆栈尺寸到TCBshr dword [es:esi+0x1a], 12 ; 登记到TCB中的尺寸要求是以4KB为单位,所以这里需除以4KBcall sel_sys_routine_seg:allocate_memoryadd eax, ecx                ; 栈顶地址(即栈基址)mov [es:esi+0x1e], eax      ; 登记0特权级堆栈基地址到TCBmov ebx, 0xf_fffe           ; 段界限mov ecx, 0x00c0_9600        ; 段属性,4KB粒度 读写 特权级DPL为0call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                ; TCB基地址call setup_ldt_descriptor; or cx, 0000_0000_0000_0000B ; 设置选择子的请求特权级RPL为0mov [es:esi+0x22], cx       ; 登记0特权级堆栈选择子到TCBmov dword [es:esi+0x24], 0  ; 登记0特权级堆栈初始esp到TCB; 创建1特权级堆栈mov ecx, 0x1000             ; 申请创建0特权级堆栈所需的4KB内存mov eax, ecx                ; 用于后面生成堆栈顶地址(即栈基址)mov [es:esi+0x28], ecx      ; 登记0特权级堆栈尺寸到TCBshr dword [es:esi+0x28], 12 ; 登记到TCB中的尺寸要求是以4KB为单位,所以这里需除以4KBcall sel_sys_routine_seg:allocate_memoryadd eax, ecx                ; 栈顶地址(即栈基址)mov [es:esi+0x2c], eax      ; 登记0特权级堆栈基地址到TCBmov ebx, 0xf_fffe           ; 段界限mov ecx, 0x00c0_b600        ; 段属性,4KB粒度 读写 特权级DPL为1call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                ; TCB基地址call setup_ldt_descriptoror cx, 0000_0000_0000_0001B ; 设置选择子的请求特权级RPL为1mov [es:esi+0x30], cx       ; 登记1特权级堆栈选择子到TCBmov dword [es:esi+0x32], 0  ; 登记1特权级堆栈初始esp到TCB; 创建2特权级堆栈mov ecx, 0x1000             ; 申请创建0特权级堆栈所需的4KB内存mov eax, ecx                ; 用于后面生成堆栈顶地址(即栈基址)mov [es:esi+0x36], ecx      ; 登记0特权级堆栈尺寸到TCBshr dword [es:esi+0x36], 12 ; 登记到TCB中的尺寸要求是以4KB为单位,所以这里需除以4KBcall sel_sys_routine_seg:allocate_memoryadd eax, ecx                ; 栈顶地址(即栈基址)mov [es:esi+0x3a], eax      ; 登记0特权级堆栈基地址到TCBmov ebx, 0xf_fffe           ; 段界限mov ecx, 0x00c0_d600        ; 段属性,4KB粒度 读写 特权级DPL为2call sel_sys_routine_seg:make_gdt_descriptormov ebx, esi                ; TCB基地址call setup_ldt_descriptoror cx, 0000_0000_0000_0010B ; 设置选择子的请求特权级RPL为2mov [es:esi+0x3e], cx       ; 登记0特权级堆栈选择子到TCBmov dword [es:esi+0x40], 0  ; 登记0特权级堆栈初始esp到TCB; 在GDT中登记LDT描述符mov eax, [es:esi+0x0c]      ; LDT起始地址movzx ebx, word [es:esi+0x0a] ; LDT段界限,movzx先零扩展再传送mov ecx, 0x0040_8200        ; LDT描述符属性,特权级DPL为0,TYPE为2表示这是一个LDT描述符call sel_sys_routine_seg:make_gdt_descriptorcall sel_sys_routine_seg:setup_gdt_descriptormov [es:esi+0x10], cx       ; 登记LDT选择子到TCB中; 创建用户程序的TSS(Task State Segment)mov ecx, 104                ; TSS的标准大小mov [es:esi+0x12], cx       dec word [es:esi+0x12]      ; 登记TSS界限值到TCB; TSS界限值必须至少是103,任何小于该值的TSS,在执行任务切换时,都会引发处理器异常中断call sel_sys_routine_seg:allocate_memory    ; 申请创建TSS所需的内存mov [es:esi+0x14], ecx      ; 登记TSS基地址到TCB; 登记基本的TSS表格内容mov word [es:ecx+0], 0      ; 将指向前一个任务的指针(任务链接域)填写为0; 这表明这是唯一的任务; 登记0/1/2特权级栈的段选择子,以及它们的初识栈指针; 所有的栈信息都在TCB中,先从TCB中取出,然后填写到TSS中的相应位置mov edx,[es:esi+0x24]       ; 登记0特权级堆栈初始ESP到TSS中mov [es:ecx+4], edx                 mov dx,[es:esi+0x22]        ; 登记0特权级堆栈段选择子到TSS中mov [es:ecx+8], dx                  mov edx,[es:esi+0x32]       ; 登记1特权级堆栈初始ESP到TSS中mov [es:ecx+12], edx                mov dx,[es:esi+0x30]        ; 登记1特权级堆栈段选择子到TSS中mov [es:ecx+16], dx                 mov edx,[es:esi+0x40]       ; 登记2特权级堆栈初始ESP到TSS中mov [es:ecx+20], edx                mov dx,[es:esi+0x3e]        ; 登记2特权级堆栈段选择子到TSS中mov [es:ecx+24], dx                     mov dx, [es:esi+0x10]       ; 登记当前任务的LDT描述符选择子到TSS中mov [es:ecx+96], dx         ; 任务切换时,处理器需要用这里的信息找到当前任务的LDT; 登记I/O许可位映射区的地址; 在这里填写的是TSS段界限(103),表明不存在该区域mov dx, [es:esi+0x12]mov [es:ecx+102], dxmov word [es:ecx+100], 0     ; T=0; 登记TSS描述符到GDT中; 和局部描述符表LDT一样,也必须在GDT中安装TSS的描述符; 一方面是为了对TSS进行段和特权级的检查,另一方面也是执行任务切换的需要; 当call far和jmp far指令的操作数是TSS描述符选择子时,处理器执行任务切换操作mov eax, [es:esi+0x14]      ; 从TCB中取得TSS的基地址movzx ebx, word [es:esi+0x12] ; TSS的界限值mov ecx, 0x0040_8900        ; TSS的属性,特权级DPL为0,字节粒度call sel_sys_routine_seg:make_gdt_descriptorcall sel_sys_routine_seg:setup_gdt_descriptormov [es:esi+0x18], cx       ; 登记TSS描述符选择子到TCB,RPL为0pop es      pop dspopadret 8       ; 丢弃调用本过程前压入的参数; 该指令执行时,除了将控制返回到过程的调用者之外,还会调整栈的指针esp=esp+8字节; 内核重新接管处理器的控制权
return_kernel:    mov eax, sel_core_data_segmov ds, eax                 ; 使ds指向mini内核数据段; 该选择子的请求特权级RPL为0,目标代码段的特权级DPL为0; 如果当前特权级CPL为3,低于目标代码段DPL,将引发处理器异常中断,也不可能通过特权级检查; mov eax, sel_core_stack_seg; mov ss, eax                 ; 使ss指向mini内核堆栈段; mov esp, [kernel_esp_pointer]mov ebx, message_kernelmode ; 显示提示信息,已返回内核态call sel_sys_routine_seg:show_string; 对于一个操作系统来说,此刻应该回收前一个用户程序所占用的内存,并启动下一个用户程序hlt     ; 进入保护模式之前,用cli指令关闭了中断,所以,; 这里除非有NMI产生,否则处理器将一直处于停机状态; Function: 在TCB链上追加任务控制块
; Input: ecx 需要追加的那项TCB线性基地址
append_to_tcb_link:push eaxpush edxpush dspush esmov eax, sel_core_data_seg  ; ds 指向内核数据段, 用于定位内核数据段中定义的TCB链表首地址tcb_chain_headmov ds, eaxmov eax, sel_mem_0_4gb_seg  ; es 指向4G内存段, 用于定位当前TCB的线性基地址mov es, eaxmov dword [es:ecx+0x00], 0  ; 将当前TCB指针域清零,表示这是链表中最后一个TCBmov eax, [tcb_chain_head]or eax, eax                 ; 判断链表是否为空jz .emptyTCB.totailTCB:mov edx, eaxmov eax, [es:edx+0x00]      ; 链表下一项TCB的指针域or eax, eaxjnz .totailTCBmov [es:edx+0x00], ecx      ; 插入至链表尾部jmp .appendTCBsucc.emptyTCB:mov [tcb_chain_head], ecx   ; 链表头部.appendTCBsucc:pop espop dspop edxpop eaxret; Function: 在ldt中安装一个新的段描述符
; Input: edx:eax 段描述符; ebx 任务控制块TCB基地址
; Output: cx 段描述符的选择子
setup_ldt_descriptor:push eaxpush edxpush edipush dsmov ecx, sel_mem_0_4gb_segmov ds, ecxmov edi, [ebx+0x0c]     ; 从用户程序的TCB中取得程序LDT基地址xor ecx, ecxmov cx, [ebx+0x0a]      ; 从用户程序的TCB中取得程序LDT界限inc cx                  ; LDT的总字节数,即新描述符偏移地址mov [edi+ecx+0x00], eaxmov [edi+ecx+0x04], edx ; 安装描述符add cx, 8               ; 每个描述符8字节dec cx                  ; 更新LDT界限值mov [ebx+0x0a], cx      ; 更新LDT界限值到用户程序的TCB中; 生成相应的段选择子; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)mov ax, cxxor dx, dxmov cx, 8                   ; 界限值总是比gdt总字节数小1。除以8,余7(丢弃不用)   div cx                      ; 商就是所需要的描述符索引号mov cx, axshl cx, 3                   ; 将索引号移到正确位置,即左移3位,留出TI位和RPL位or cx, 0000_0000_0000_0100B ; 这里 TI=1, 指向ldt; RPL=000; 于是生成了相应的段选择子    pop dspop edipop edxpop eaxretcore_code_end:; ===============================================================================
SECTION core_data vstart=0               ; mini内核数据段; sgdt, Store Global Descriptor Table Register
; 将gdtr寄存器的基地址和边界信息保存到指定的内存位置
; 低2字节为gdt界限(大小),高4字节为gdt的32位物理地址
; lgdt, load gdt, 指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0; 内存分配时的起始地址
; 每次请求分配内存时,返回这个值,作为所分配内存的起始地址;
; 同时,将这个值加上所分配的长度,作为下次分配的起始地址写回该内存单元
ram_allocate_base dd 0x0010_0000; 系统API的符号-地址检索表
; 自命名 Symbol-Address Lookup Table, SALT
sys_api:sys_api_1  db '@ShowString'times 256-($-sys_api_1) db 0dd show_stringdw sel_sys_routine_segsys_api_2  db '@ReadDiskData'times 256-($-sys_api_2) db 0dd read_hard_disk_0dw sel_sys_routine_segsys_api_3  db '@ShowDwordAsHexString'times 256-($-sys_api_3) db 0dd show_hex_dworddw sel_sys_routine_seg            sys_api_4  db '@TerminateProgram'times 256-($-sys_api_4) db 0dd return_kerneldw sel_core_code_segsys_api_item_length     equ $-sys_api_4
sys_api_items           equ ($-sys_api)/sys_api_item_length    ; 提示信息,内核已加载成功并开始执行
message_kernel_load_succ db '  If you seen this message,that means we 'db 'are now in protect mode,and the system 'db 'core is loaded,and the video display 'db 'routine works perfectly.', 0x0d, 0x0a, 0; 提示信息,开始加载用户程序
message_app_load_begin  db '  Loading user program...', 0; 提示信息,用户程序加载并重定位完成
message_app_load_succ   db 'Done.', 0x0d, 0x0a, 0message_kernelmode      db  0x0d,0x0a,0x0d,0x0a,0x0d,0x0adb  '  User program terminated,control returned.',0;提示信息,系统api的调用门安装完成
message_callgate_mount_succ db '  System wide CALL-GATE mounted.',0x0d,0x0a,0; 处理器品牌信息
cpu_brand0  db 0x0d, 0x0a, '  ', 0      ; 空行
cpu_brand   times 52 db 0
cpu_brand1  db 0x0d, 0x0a, 0x0d, 0x0a, 0; 空行core_buf    times 2048 db 0             ; 自定义的内核缓冲区kernel_esp_pointer  dd 0                ; 临时保存内核的堆栈指针bin_hex     db '0123456789ABCDEF'       ; show_hex_dword过程需要的查找表; 任务控制块TCB链表
tcb_chain_head   dd 0core_data_end:; ===============================================================================
SECTION sys_routine vstart=0               ; 系统api代码段; Function: 频幕上显示文本,并移动光标
; Input: ds:ebx 字符串起始地址,以0结尾
show_string:push ecx.loop_show_string:mov cl, [ebx]or cl, cljz .exit                ; 以0结尾call show_charinc ebxjmp .loop_show_string.exit:pop ecxretf                    ; 段间调用返回; Function:
; Input: cl 字符
show_char:; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDIpushad; 读取当前光标位置; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位; 数据端口0x3d5mov dx, 0x3d4   mov al, 0x0e   out dx, almov dx, 0x3d5in al, dxmov ah, almov dx, 0x3d4mov al, 0x0fout dx, almov dx, 0x3d5in al, dxmov bx, ax      ; 此处用bx存放光标位置的16位数; 判断是否为回车符0x0dcmp cl, 0x0d    ; 0x0d 为回车符jnz .show_0a    ; 不是回车符0x0d,再判断是否换行符0x0amov ax, bx      ; 是回车符,则将光标置位到行首mov bl, 80div blmul blmov bx, axjmp .set_cursor; ; 将光标位置移到行首,可以直接减去当前行吗??; mov ax, bx; mov dl, 80; div dl; sub bx, ah; jmp .set_cursor; 判断是否为换行符0x0a.show_0a:cmp cl, 0x0a    ; 0x0a 为换行符    jnz .show_normal; 不是换行符,则正常显示字符add bx, 80      ; 是换行符,再判断是否需要滚屏jmp .roll_screen; 正常显示字符; 在写入其它内容之前,显存里全是黑底白字的空白字符0x0720,所以可以不重写黑底白字的属性.show_normal:push esmov eax, sel_video_ram_seg  ; 0xb8000段的选择子,显存映射在 0xb8000~0xbffffmov es, eaxshl bx, 1       ; 光标指示字符位置,显存中一个字符占2字节,光标位置乘2得到该字符在显存中得偏移地址    mov [es:bx], clpop esshr bx, 1       ; 恢复bxinc bx          ; 将光标推进到下一个位置; 判断是否需要向上滚动一行屏幕.roll_screen:cmp bx, 2000    ; 25行x80列jl .set_cursorpush dspush esmov eax, sel_video_ram_seg    mov ds, eax      ; movsd的源地址ds:esimov es, eax      ; movsd的目的地址es:edimov esi, 0xa0mov edi, 0cld             ; 传送方向cls stdmov cx, 1920    ; rep次数 24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节rep movsd; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720mov bx, 3840    ; 24行*每行80个字符*每个字符加显示属性占2字节mov cx, 80.cls:mov word [es:bx], 0x0720add bx, 2loop .clspop espop dsmov bx, 1920    ; 重置光标位置为最底一行行首; 根据bx重置光标位置; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位; 数据端口0x3d5.set_cursor:mov dx, 0x3d4   mov al, 0x0e   out dx, almov dx, 0x3d5mov al, bh      ; in和out 只能用al或者axout dx, almov dx, 0x3d4mov al, 0x0fout dx, almov dx, 0x3d5mov al, blout dx, al; 依次pop EDI,ESI,EBP,EBX,EDX,ECX,EAXpopadret; ===============================================================================
; Function: 读取主硬盘的1个逻辑扇区
; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址
read_hard_disk_0:push eaxpush ebxpush ecxpush edxpush eax; 1) 设置要读取的扇区数; ==========================; 向0x1f2端口写入要读取的扇区数。每读取一个扇区,数值会减1;; 若读写过程中发生错误,该端口包含着尚未读取的扇区数mov dx, 0x1f2           ; 0x1f2为8位端口mov al, 1               ; 1个扇区out dx, al; 2) 设置起始扇区号; ===========================; 扇区的读写是连续的。这里采用早期的LBA28逻辑扇区编址方法,; 28个比特表示逻辑扇区号,每个扇区512字节,所以LBA25可管理128G的硬盘; 28位的扇区号分成4段,分别写入端口0x1f3 0x1f4 0x1f5 0x1f6,都是8位端口inc dx                  ; 0x1f3pop eaxout dx, al              ; LBA地址7~0inc dx                  ; 0x1f4mov cl, 8shr eax, clout dx, al              ; in和out 操作寄存器只能是al或者ax; LBA地址15~8inc dx                  ; 0x1f5shr eax, clout dx, al              ; LBA地址23~16; 8bits端口0x1f6,低4位存放28位逻辑扇区号的24~27位;; 第4位指示硬盘号,0为主盘,1为从盘;高3位,111表示LBA模式inc dx                  ; 0x1f6shr eax, cl             or al, 0xe0             ; al 高4位设为 1110; al 低4位设为 LBA的的高4位out dx, al; 3) 请求读硬盘; ==========================; 向端口写入0x20,请求硬盘读inc dx                  ; 0x1f7mov al, 0x20out dx, al.wait:; 4) 等待硬盘读写操作完成; ===========================; 端口0x1f7既是命令端口,又是状态端口; 通过这个端口发送读写命令之后,硬盘就忙乎开了。; 0x1f7端口第7位,1为忙,0忙完了同时将第3位置1表示准备好了,; 即0x08时,主机可以发送或接收数据in al, dx               ; 0x1f7and al, 0x88            ; 取第8位和第3位cmp al, 0x08            jnz .wait; 5) 连续取出数据; ============================; 0x1f0是硬盘接口的数据端口,16bitsmov ecx, 256             ; loop循环次数,每次读取2bytesmov dx, 0x1f0           ; 0x1f0.readw:in ax, dxmov [ebx], axadd ebx, 2loop .readwpop edxpop ecxpop ebxpop eaxretf        ; 段间返回; ===============================================================================
; Function: 分配内存
; Input: ecx 希望分配的字节数
; Output: ecx 起始地址
allocate_memory:push eaxpush ebxpush dsmov eax, sel_core_data_segmov ds, eax                     ; 切换ds到内核数据段mov eax, [ram_allocate_base]add eax, ecx                    ; 下次分配时的起始地址    ; 这里应当检测可用内存数量,但本程序很简单,就忽略了mov ecx, [ram_allocate_base]    ; 返回分配的起始地址; 4字节对齐下次分配时的起始地址, 即最低2位为0; 32位的系统建议内存地址最好是4字节对齐,这样访问速度能最快mov ebx, eaxand ebx, 0xffff_fffcadd ebx, 4                      ; 4字节对齐test eax, 0x0000_0003           ; 判断是否对齐cmovnz eax, ebx                 ; 如果非零,即没有对齐,则强制对齐; cmovcc避免了低效率的控制转移mov [ram_allocate_base], eax    ; 下次分配时的起始地址pop dspop ebxpop eaxretf        ; retf指令返回,因此只能通过远过程调用来进入; ===============================================================================
; Function: 构造段描述符
; Input: 1) eax 线性基地址 2) ebx 段界限 3) ecx 属性(无关位则置0)
; Output: edx:eax 完整的8字节(64位)段描述符
make_gdt_descriptor:; 构造段描述符的低32位; 低16位,为段界限的低16位; 高16位,为段基址的低16位mov edx, eaxshl eax, 16or ax, bx           ; 段描述符低32位(eax)构造完毕; 段基地址在描述符高32位edx两边就位and edx, 0xffff0000 ; 清除基地址的低32位(低32位前面已处理完成)    rol edx, 8          ; rol循环左移bswap edx           ; bswap, byte swap 字节交换; 段界限的高4位在描述符高32位中就位and ebx, 0x000f0000 ; 20位的段界限只保留高4位(低16位前面已处理完成)or edx, ebx; 段属性在描述符高32位中就位or edx, ecx         ; 入参的段界限ecx无关位需先置0retf; ===============================================================================
; Function: 在gdt中安装一个新的段描述符
; Input: edx:eax 段描述符
; Output: cx 段描述符的选择子
setup_gdt_descriptor:push eaxpush ebxpush edxpush dspush esmov ebx, sel_core_data_seg  ; 切换ds到内核数据段mov ds, ebx; sgdt, Store Global Descriptor Table Register; 将gdtr寄存器的基地址和边界信息保存到指定的内存位置; 低2字节为gdt界限(大小),高4字节为gdt的32位物理地址sgdt [gdt_size]mov ebx, sel_mem_0_4gb_segmov es, ebx                 ; 使es指向4GB内存段以操作全局描述符表gdt; movzx, Move with Zero-Extend, 左边添加0扩展; 或使用这2条指令替换movzx指令 xor ebx, ebx; mov bx, [gdt_size]movzx ebx, word [gdt_size]  ; gdt界限inc bx                      ; gdt总字节数,也是gdt中下一个描述符的偏移; 若使用inc ebx, 如果是启动计算机以来第一次在gdt中安装描述符就会有问题add ebx, [gdt_base]         ; 下一个描述符的线性地址mov [es:ebx], eaxmov [es:ebx+4], edxadd word [gdt_size], 8      ; 将gdt的界限值加8,每个描述符8字节; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址; GDTR, 全局描述符表寄存器lgdt [gdt_size]             ; 对gdt的更改生效; 生成相应的段选择子; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)mov ax, [gdt_size]xor dx, dxmov bx, 8                   ; 界限值总是比gdt总字节数小1。除以8,余7(丢弃不用)   div bx                      ; 商就是所需要的描述符索引号mov cx, axshl cx, 3                   ; 将索引号移到正确位置,即左移3位,留出TI位和RPL位; 这里 TI=0, 指向gdt RPL=000; 于是生成了相应的段选择子pop espop dspop edxpop ebxpop eaxretf; ===============================================================================
; Function: 将ds的值以十六进制的形式在屏幕上显示
; Input:
; Output:
show_hex_dword:; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDIpushadpush dsmov ax, sel_core_data_segmov ds, axmov ebx, bin_hexmov ecx, 8              ; 循环8次.hex2word:rol edx, 4              ; 循环左移mov eax, edxand eax, 0x0000_000f; xlat, 处理器的查表指令; 用al作为偏移量,从ds:ebx指向的内存空间中取出一个字节,传回alxlatpush ecxmov cl, alcall show_char          ; 显示pop ecxloop .hex2wordpop dspopadretf; ===============================================================================
; Function: 构造调用门的门描述符
; Input: eax 门代码在段内的偏移地址; bx 门代码所在段的段选择子; cx 门属性
; Output: edx:eax 门描述符
make_gate_descriptor:    push ebxpush ecxmov edx, eaxand edx, 0xffff_0000    ; 得到偏移地址高16位    or dx, cx               ; 组装属性部分到edxand eax, 0x0000_ffff    ; 得到偏移地址低16位shl ebx, 16or eax, ebx             ; 组装段选择子到eaxpop ecxpop ebxretf                ; retf 说明该过程必须以远调用的方式使用sys_routine_end:; ===============================================================================
SECTION tail        ; 这里用于计算程序大小,不需要vstart=0
core_end:

[书]x86汇编语言:从实模式到保护模式 -- 第14章 任务和特权级保护,调用门、LDT、TSS、TCB相关推荐

  1. 第14章 任务和特权级保护

    学习这一章感觉异常的困难,所以学习从14-17章,每一章学扎实了,弄懂了每个问题再进行下一章,后一章都是在前一章的基础上增加一些数据结构和机制.另外读的时候可以各个击破,每次只搞明白一个小问题.读这一 ...

  2. [书]x86汇编语言:从实模式到保护模式 -- 第16章 分页机制、平坦模型

    # 分页机制 二级页表:页目录.页表 ==> 4KB物理页 32位线性地址中:高10位为页目录中的索引号(乘4得偏移量),该目录项指向页表的基地址:中间10位为页表中的索引号,该页表项指向4KB ...

  3. [书]x86汇编语言:从实模式到保护模式 -- 第15章 任务切换

    # 执行结果 # TODO:字符串显示函数的滚屏部分应该是有bug. # file_02: c15_core.asm ; FILE: c13_core.asm ; DATE: 20200104 ; T ...

  4. [书]x86汇编语言:从实模式到保护模式 -- 第17章 中断、任务切换、分页机制、平坦模型

    # 任务切换 内核任务.用户任务1.用户任务2,之前的轮询切换 利用RTC芯片的硬件中断来实现任务切换 计算机主板上有实时时钟芯片RTC,可以设置RTC芯片,使得它每次更新CMOS中的时间信息后,发出 ...

  5. [书]x86汇编语言:从实模式到保护模式 -- 第13章 mbr加载内核、内核加载应用程序

    # mbr加载内核 1.0x7c00,16位实模式 2.进入保护模式前的准备工作:创建段描述符(代码段.数据段.堆栈段.显示缓冲区),构建gdt 3.进入保护模式 ; 开启保护模式 ; CR0的第1位 ...

  6. [书]x86汇编语言:从实模式到保护模式 -- 第11章 进入保护模式,初识全局描述符表GDT; 第12章 别名,冒泡排序

    第11章 进入保护模式:初始化全局描述符表,通过GDT进入代码段.数据段.堆栈段 ; FILE: c11_mbr.asm ; DATE: 20191229 ; TITLE: 硬盘主引导扇区代码; 设置 ...

  7. [书]x86汇编语言:从实模式到保护模式 -- 第九章 硬中断,使用RTC芯片实现实时时间的显示;软中断,使用BIOS中断实现键盘输入的读取和显示

    PART 1 >> 使用BIOS中断实现键盘输入的读取和显示 ; File: c09_2.asm ; Date: 20191222; =========================== ...

  8. [书]x86汇编语言:从实模式到保护模式 -- 第八章 硬盘和显卡的访问与控制,mbr加载并重定位应用程序

    第八章 硬盘和显卡的访问与控制 mbr加载.重定位用户程序 PART 1 >> VirtualBox显示最终效果 ===================================== ...

  9. [书]x86汇编语言:从实模式到保护模式 -- 第六、七章 编写主引导扇区代码

    第六章 编写主引导扇区代码(启动时显示文字:Label offset:) PART 1 >> 用VirtualBox显示最终效果 1.1 汇编 启用nasm的工具"nasm-sh ...

最新文章

  1. 网上看的一篇文章,感觉会给程序员一些启发
  2. 类属性的存储和this指针
  3. 操作系统(1) -- 计算机系统概述
  4. jvm内存参数配置_idea中设置JVM参数,简单理解JVM常见参数,JVM调优简单入门
  5. nohup 命令 用途:不挂断地运行命令
  6. mac os 开启redis_高并发大流量,总会想到它!来一起通过docker搭建redis集群
  7. 01慕课网《进击Node.js基础(一)》Node.js安装,创建例子
  8. shell脚本不换行刷新数据
  9. WPE下载 WPE 各版本下载
  10. matlab 7.0电路图,基于Multisim10和Matlab7.0的正弦稳态电路分析
  11. java8 131下载_jdk 8u131下载
  12. 网站SEO过程中的死链处理
  13. firefox浏览器上安装selenium IDE插件
  14. 什么是内部类,以及内部类的特点
  15. 北航计算机刘强,刘强 LIU Qiang
  16. 2020年回顾 | 华清远见研发中心2020年终盘点
  17. 阿里健康2021实习生招聘
  18. 远大国际期货交易平台
  19. 微信查询所有关注该公众号的用户
  20. 语义分割模型中分辨率恢复_语义模型在智慧工业运营中的作用

热门文章

  1. VC++中利用/GS开关防止缓冲区溢出
  2. 咬文嚼字之 Retrofit2 使用
  3. AD 和 DA-基本概念
  4. 购置税用计算机怎么算百分比,怎样在网上使用新车购置税计算器?
  5. web前端开发日记------入职腾讯外包
  6. PHP入门-PHP OOP编程
  7. 15-多对多做左连接查询(查询老师,并且把关联的学生也查出来)
  8. oracle rac 环境配置文件,学习笔记:Oracle RAC spfile参数文件配置案例详解
  9. 中国石油大学(北京)-《 西方艺术赏析》-第一阶段在线作业
  10. 原生API编写简单富文本编辑器001