|
Posted on 2009-02-03 14:38 bpt 阅读(1270) 评论(2) 编辑 收藏 引用
将自制的操作系统命名为BlackPoint 基本是按照《自己动手写os》中的步骤来的,从软盘启动,调入Loader再调入Kernel 没有完全复制书中的代码,而是按照其中的思路来自己写一遍,收获颇丰啊。 第一步先构造软盘的启动扇区boot.asm,使用NASM在XP下开发:
1 %include "address.inc" 2 %include "macro.inc" 3 4 org 0x7c00 5 TOP_OF_STACK EQU 0x7c00 6 7 jmp short LAB_boot 8 nop 9 10 %include "fat12hdr" 11 12 LAB_boot: 13 mov ax, cs 14 mov ds, ax 15 mov es, ax 16 mov ss, ax 17 mov sp, TOP_OF_STACK 18 19 RESET_FLOPPY 20 21 ;load FAT1 22 LOAD_SEC SBASE_FAT, START_FAT1, LENGTH_FAT1 23 24 ;load Root Entry Table 25 LOAD_SEC SBASE_RET, START_RET, LENGTH_RET 26 27 ;load loader 28 LOAD_FILE SBASE_loader, OFFSET_loader, loader_filename 29 30 jmp SBASE_loader:OFFSET_loader 31 32 33 ;strings 34 loader_filename DB `LOADER BIN`, 0 35 36 %include "floppy.asm" 37 38 TIMES (510 - ($ - $$)) DB 0 39 DW 0xaa55
启动扇区包含了四个文件:address是系统的内存布局安排,macro是我为了偷懒定义的一些宏。 fat12hdr是fat12格式软盘的头信息。 而floppy.asm是一些写好的过程,因为loader中也要用到,所以干脆做成了一个文件。 下面是floppy.asm的代码,他跟fat12hdr的依赖度很高:
1 %include "address.inc" 2 3 ;--------------------------------------------------------------------- 4 5 ;subroutine ReadSector 6 ;read bl Sections start from Section ax 7 ;result will be in es:di 8 ReadSector: 9 10 mov cl, 18 11 div cl 12 inc ah 13 mov cl, ah ;get Section 14 mov ch, al 15 shr ch, 1 ;get Cylinder 16 mov dh, al 17 and dh, 1 ;get Head 18 xor dl, dl ;disk0 19 20 mov bh, 0x2 21 push bp 22 push bx 23 mov bp, sp 24 mov bx, di 25 .1: 26 mov ax, word [bp] 27 int 0x13 28 test ah, ah 29 jnz .1 30 pop bx 31 pop bp 32 ret 33 34 ;--------------------------------------------------------------------- 35 36 ;subroutine SearchFile 37 ;search file in root entry table (SBASE_RET:0) 38 ;input: filename should be stored in ds:si 39 ;output: ax will be the first cluster no. of the file 40 SearchFile: 41 mov bx, si ;ds:bx -> filename 42 mov ax, SBASE_RET 43 mov es, ax 44 xor di, di ;es:0 -> the first Root Entry 45 mov dx, 224 46 .search: 47 mov cx, 11 48 .compare: 49 mov al, byte [ds:bx] 50 cmp al, byte [es:di] 51 jne .next_entry 52 inc bx 53 inc di 54 loop .compare 55 jmp .ok 56 .next_entry: 57 mov bx, si ;ds:bx -> filename 58 and di, 0xffe0 59 add di, 32 ;es:di -> next Root Entry 60 dec dx 61 jnz .search 62 .fail: 63 mov ax, cs 64 mov ds, ax 65 mov si, .msg_fail 66 .print: 67 mov al, byte [ds:si] 68 test al, al 69 jz $ 70 mov ah, 0x0e 71 int 0x10 72 inc si 73 jmp .print 74 .ok: 75 and di, 0xffe0 76 mov ax, word [es:di + 0x1a] ;get cluster no. 77 ret 78 79 ;strings 80 .msg_fail DB ` file is missing\n\r`, 0 81 82 ;--------------------------------------------------------------------- 83 84 ;subroutine LoadFile 85 ;load file started from cluster ax to es:di 86 LoadFile: 87 .1: 88 cmp ax, 0xff8 89 jae .end 90 push ax 91 add ax, 31 92 mov bl, 1 93 call ReadSector 94 pop ax 95 call GetNextClus 96 add di, 512 97 jmp .1 98 .end: 99 ret 100 101 ;--------------------------------------------------------------------- 102 103 ;subroutine GetNextClus 104 ;get next clus_num of current clus(stored in ax) 105 ;FAT must be loaded in SBASE_FAT:0 106 ;result will be in ax 107 ;none of regs will be changed besides ax 108 GetNextClus: 109 push ds 110 push si 111 push bx 112 113 mov bx, SBASE_FAT 114 mov ds, bx 115 mov bx, ax ;let bx be the cluster no. 116 shr ax, 1 117 imul ax, 3 118 mov si, ax 119 test bx, 1 ;test cluster no. whether even or odd 120 jnz .odd 121 .even: 122 mov ax, word [ds:si] 123 and ax, 0xfff 124 jmp .end 125 .odd: 126 mov ax, word [ds:si + 1] 127 shr ax, 4 128 .end: 129 pop bx 130 pop si 131 pop ds 132 ret
下面是fat12hdr的内容,抄自《自己动手写os》
1 BS_OEMName DB 'bpt ' 2 BPB_BytsPerSec DW 512 3 BPB_SecPerClus DB 1 4 BPB_RsvdSecCnt DW 1 5 BPB_NumFATs DB 2 6 BPB_RootEntCnt DW 224 7 BPB_TotSec16 DW 2880 8 BPB_Media DB 0xF0 9 BPB_FATSz16 DW 9 10 BPB_SecPerTrk DW 18 11 BPB_NumHeads DW 2 12 BPB_HiddSec DD 0 13 BPB_TotSec32 DD 0 14 BS_DrvNum DB 0 15 BS_Reserved1 DB 0 16 BS_BootSig DB 29h 17 BS_VolID DD 0 18 BS_VolLab DB 'BlackPoint ' 19 BS_FileSysType DB 'FAT12 ' 20 21 %ifndef __FAT12HDR 22 %define __FAT12HDR 23 24 START_FAT1 EQU 1 ;BPB_RsvdSecCnt 25 LENGTH_FAT1 EQU 9 ;BPB_FATSz16 26 START_RET EQU 19 ;BPB_RsvdSecCnt + BPB_NumFATs * BPB_FATSz16 27 LENGTH_RET EQU 14 ;BPB_RootEntCnt * 32 / BPB_BytsPerSec 28 29 %endif
下面是address.inc,其中的内容是启动时os的内存安排
1 %ifndef __ADDRESS_INC 2 %define __ADDRESS_INC 3 4 SBASE_FAT EQU 0x8000 5 SBASE_RET EQU 0x8500 6 7 SBASE_loader EQU 0x1000 8 OFFSET_loader EQU 0x00100 9 BASE_loader EQU 0x10000 10 11 SBASE_mem EQU 0x9000 12 BASE_mem EQU 0x90000 13 ;name ;offset length note 14 mem_size EQU 0 ;4 memory size 15 num_ards EQU 4 ;4 number of ards 16 mem_info EQU 8 ;20 x 50 memory block info 17 18 SBASE_elf EQU 0x2000 19 OFFSET_elf EQU 0x0 20 BASE_elf EQU 0x20000 21 22 BASE_kernel EQU 0x30000 23 KERNEL_ENTRY EQU 0x30400 24 25 BASE_page_dir EQU 0x100000 26 BASE_page_table EQU 0x101000 27 28 %endif
在下面就是我用来偷懒的macro.inc
1 %ifndef __MACRO_INC 2 %define __MACRO_INC 3 4 ;--------------------------------------------------------------------- 5 6 %macro RESET_FLOPPY 0 7 xor ah, ah 8 xor dl, dl 9 int 0x13 10 11 %endmacro 12 13 ;--------------------------------------------------------------------- 14 15 %macro CLOSE_FLOPPY 0 16 mov dx, 0x3f2 17 xor al, al 18 out dx, al 19 %endmacro 20 21 ;--------------------------------------------------------------------- 22 23 ;load %3 sectors from %2 to %1:0 24 %macro LOAD_SEC 3 25 push es 26 mov ax, %1 27 mov es, ax 28 xor di, di 29 mov ax, %2 30 mov bl, %3 31 call ReadSector 32 pop es 33 %endmacro 34 35 ;--------------------------------------------------------------------- 36 37 ;load file(ds:%3) to %1:%2 38 %macro LOAD_FILE 3 39 mov si, %3 40 call SearchFile 41 push es 42 mov bx, %1 43 mov es, bx 44 mov di, %2 45 call LoadFile 46 pop es 47 %endmacro 48 49 ;--------------------------------------------------------------------- 50 51 ;PRINT row(byte), col(byte), char(byte) 52 %macro PRINT 3 53 mov edi, (80 * (%1) + (%2)) * 2 54 mov ah, 0x0c 55 mov al, (%3) 56 mov [gs:edi], ax 57 %endmacro 58 59 ;--------------------------------------------------------------------- 60 61 %endif
折腾了这么多,软盘启动扇区算是开发完了。一路下来算是会用NASM了。记得刚开始用NASM时那个难啊。其实万事开头难,在暴风雨中坚持住,阳光自己来。 接下来是Loader的开发,更是让我叫苦不迭,每走一步都是荆棘遍野。通常是后面的bug调好了又发现了前面的bug,甚至多次重写。好在loader功能不多,可以一个一个小模块的开发,再组合起来。 先亮出一个重要的头文件,与保护模式有关的宏和定义都放置在x86.inc中:
1 %ifndef __X86_INC 2 %define __X86_INC 3 4 %define BIT(X) (1 << (X)) 5 6 ;=============================================================================== 7 8 ;despcritor des_t base(dword), limit(dword), attribute(word) 9 ; 10 ;attribute: 11 ; 11 10 9 8 7 6 5 4 3 2 1 0 12 ; G D 0 0 P <DPL> S < type > A 13 %macro des_t 3 14 DW ((%2) & 0xffff) ;limit 15~0 15 DW ((%1) & 0xffff) ;base 15~0 16 DB (((%1) >> 16) & 0xff) ;base 23~16 17 DB ((%3) & 0xff) ;P, DPL, S, type, A 18 DB ((((%2) >> 16) & 0xf) | (((%3) >> 4) & 0xf0)) 19 ;G, D, 0, 0, limit 19~16 20 DB (((%1) >> 24) & 0xff) ;base 31~24 21 %endmacro 22 23 ;descriptor attribute 24 DA_G EQU BIT(11) ;granularity 4KB 25 DA_4G EQU DA_G 26 DA_D EQU BIT(10) ;default operation size 32bits 27 DA_B EQU DA_D 28 DA_P EQU BIT(7) ;segment present 29 DA_S EQU BIT(4) 30 DA_DATA EQU DA_S ;data segment 31 DA_E EQU BIT(2) 32 DA_W EQU BIT(1) ;(data segment)writable 33 DA_CODE EQU DA_S | BIT(3) ;code segment 34 DA_C EQU BIT(2) ;(code segment)readable 35 DA_R EQU BIT(1) ;(code segment)conforming 36 DA_A EQU BIT(0) ;Accessed 37 38 DA_LDT EQU 0x2 | DA_P 39 DA_TSS EQU 0x9 | DA_P 40 DA_CODE16 EQU DA_CODE | DA_P 41 DA_CODE32 EQU DA_CODE | DA_D | DA_P 42 DA_CODE32C EQU DA_CODE32 | DA_C 43 DA_CODE32R EQU DA_CODE32 | DA_R 44 DA_CODE32CR EQU DA_CODE32C | DA_R 45 DA_CODE32RC EQU DA_CODE32CR 46 DA_DATA16 EQU DA_DATA | DA_P 47 DA_DATA16W EQU DA_DATA16 | DA_W 48 DA_DATA32 EQU DA_DATA | DA_B | DA_P 49 DA_DATA32W EQU DA_DATA32 | DA_W 50 DA_STACK16 EQU DA_DATA | DA_W | DA_P 51 DA_STACK32 EQU DA_DATA | DA_W | DA_B | DA_P 52 53 DA_DPL0 EQU 0000_0000b ;descriptor privilege level 54 DA_DPL1 EQU 0010_0000b 55 DA_DPL2 EQU 0100_0000b 56 DA_DPL3 EQU 0110_0000b 57 58 59 ;FILL_DES_BASE descriptor, segment(word), offset(word) 60 ;fill descriptor's dase with segment:offset 61 ;eax will be modified 62 %macro FILL_DES_BASE 3 63 xor eax, eax 64 mov ax, %2 65 shl eax, 4 66 add eax, %3 67 mov word [%1 + 2], ax 68 shr eax, 16 69 mov byte [%1 + 4], al 70 mov byte [%1 + 7], ah 71 %endmacro 72 73 ;FILL_DES_LIMIT descriptor, limit(dword) 74 ;fill descriptor's limit 75 ;eax will be modified 76 %macro FILL_DES_LIMIT 2 77 mov eax, %2 78 mov word [%1], ax 79 shr eax, 16 80 and al, 0xf 81 or byte [(%1) + 6], al 82 %endmacro 83 84 ;=============================================================================== 85 86 ;gate_t selector(word), offset(dword), param_count(byte), attribute(byte) 87 %macro gate_t 4 88 DW ((%2) & 0xffff) ;offset 15~0 89 DW ((%1) & 0xffff) ;selector 90 DB ((%3) & 0x11111b) ;param count 91 DB ((%4) & 0xff) ;P, DPL, S, type 92 DW (((%2) >> 16) & 0xffff) ;offset 31~24 93 %endmacro 94 95 ;gate attribute 96 GA_P EQU BIT(7) ;gate present 97 98 GA_CALL32 EQU 0xc | GA_P 99 GA_INT32 EQU 0xe | GA_P 100 GA_TRAP32 EQU 0xf | GA_P 101 102 GA_DPL0 EQU 0000_0000b ;gate privilege level 103 GA_DPL1 EQU 0010_0000b 104 GA_DPL2 EQU 0100_0000b 105 GA_DPL3 EQU 0110_0000b 106 107 ;=============================================================================== 108 109 ;DEFINE_SELECTOR name, offset(word), attribute(byte) 110 %macro DEFINE_SELECTOR 3 111 %1 EQU (((%2) & 1111_1000b) | (%3)) 112 %endmacro 113 114 ;selector attribute 115 SA_TI EQU BIT(2) 116 SA_GDT EQU 0 117 SA_LDT EQU SA_TI 118 119 SA_RPL0 EQU 0 120 SA_RPL1 EQU 1 121 SA_RPL2 EQU 2 122 SA_RPL3 EQU 3 123 124 ;=============================================================================== 125 126 ;Page Directory/Table Entry 127 PG_P EQU BIT(0) 128 PG_R EQU 0 129 PG_W EQU BIT(1) 130 PG_S EQU 0 131 PG_U EQU BIT(2) 132 ;not complete 133 134 ;=============================================================================== 135 136 %undef BIT 137 138 %endif
刚开始接触保护模式时,被成堆的资料雷住了... 其实后来发现我接触的保护模式并不难,因为我只用了其中一小部分,只要能把参考文档看懂,都是死东西。主要内容按照《自己动手写os》第3章各节的内容练一遍,再准备好汇编黑皮书和Intel的开发者手册,基本上可以过关。 有了保护模式的一些知识,就开始着手写loader:
1 %include "address.inc" 2 %include "macro.inc" 3 %include "x86.inc" 4 5 org 0x100 6 TOP_OF_STACK EQU 0x100 7 8 ;=============================================================================== 9 LAB_loader: 10 mov ax, cs 11 mov ds, ax 12 mov es, ax 13 mov ss, ax 14 mov sp, TOP_OF_STACK 15 16 RESET_FLOPPY 17 LOAD_FILE SBASE_elf, OFFSET_elf, kernel_filename 18 CLOSE_FLOPPY 19 20 call CheckMem 21 shr eax, 20 22 call PrintEAX 23 mov si, msg_RAM 24 call PrintString 25 26 lgdt [gdtr] 27 28 cli 29 30 in al, 0x92 31 or al, 0000_0010b 32 out 0x92, al 33 34 mov eax, cr0 35 or eax, 1 36 mov cr0, eax 37 38 jmp dword gsel_flat_code:(BASE_loader + section.SEG_code32.start) 39 40 ;strings 41 kernel_filename DB "KERNEL BIN", 0 42 msg_RAM DB `MB Memory\n\r`, 0 43 44 ;--------------------------------------------------------------------- 45 46 ;subroutine CheckMem 47 ;none of segment regs will be modified 48 CheckMem: 49 push ds 50 push es 51 52 mov ax, SBASE_mem 53 mov ds, ax 54 mov es, ax 55 xor ebx, ebx 56 mov edi, mem_info 57 .info: 58 mov eax, 0xe820 59 mov ecx, 20 60 mov edx, 0x534D4150 61 int 0x15 62 jc .fail 63 add edi, 20 64 inc dword [num_ards] 65 test ebx, ebx 66 jnz .info 67 jmp .ok 68 .fail: 69 mov ax, cs 70 mov ds, ax 71 mov si, .msg_fail 72 .print: 73 mov al, byte [ds:si] 74 test al, al 75 jz $ 76 mov ah, 0x0e 77 int 0x10 78 inc si 79 jmp .print 80 .ok: 81 mov ecx, dword [num_ards] 82 mov si, mem_info 83 .count: 84 cmp dword [si + 16], 1 85 jne .next 86 mov eax, dword [si] 87 add eax, dword [si + 8] 88 cmp eax, dword [mem_size] 89 jna .next 90 mov dword [mem_size], eax 91 .next: 92 add si, 20 93 loop .count 94 95 mov eax, dword [mem_size] 96 pop es 97 pop ds 98 ret 99 100 ;strings 101 .msg_fail DB `memeory check error\n\r`, 0 102 103 ;--------------------------------------------------------------------- 104 105 ;subroutine PrintEAX 106 ;none of regs will be modified 107 PrintEAX: 108 push eax 109 push ebx 110 push ecx 111 push edx 112 113 xor ecx, ecx 114 .cal: 115 xor edx, edx 116 mov ebx, 10 117 div ebx 118 push edx 119 inc ecx 120 test eax, eax 121 jnz .cal 122 .print: 123 pop eax 124 add al, '0' 125 call PrintAL 126 loop .print 127 128 pop edx 129 pop ecx 130 pop ebx 131 pop eax 132 ret 133 134 ;--------------------------------------------------------------------- 135 136 ;subroutine PrintString 137 ;ds:si should be the address of the string end of 0 138 ;none of regs will be modified 139 PrintString: 140 push si 141 push ax 142 .1: 143 mov al, byte [ds:si] 144 test al, al 145 jz .end 146 call PrintAL 147 inc si 148 jmp .1 149 .end: 150 pop ax 151 pop si 152 ret 153 154 ;--------------------------------------------------------------------- 155 156 PrintAL: 157 push ax 158 mov ah, 0xe 159 int 0x10 160 pop ax 161 ret 162 163 ;---------------------------------------------------------------------- 164 165 %include "floppy.asm" 166 167 ;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 168 169 ;=============================================================================== 170 SECTION SEG_gdt align=8 171 172 gdes_null des_t 0, 0, 0 173 gdes_flat_code des_t 0, 0xfffff, DA_CODE32R | DA_4G | DA_DPL0 174 gdes_flat_data des_t 0, 0xfffff, DA_DATA32W | DA_4G | DA_DPL0 175 gdes_video des_t 0xb8000, 0xffff, DA_DATA16W | DA_DPL3 176 177 gdtr DW $ - $$ - 1 178 DD BASE_loader + $$ 179 180 DEFINE_SELECTOR gsel_flat_code, gdes_flat_code - $$, SA_GDT | SA_RPL0 181 DEFINE_SELECTOR gsel_flat_data, gdes_flat_data - $$, SA_GDT | SA_RPL0 182 DEFINE_SELECTOR gsel_video, gdes_video - $$, SA_GDT | SA_RPL0 183 ;SECTION SEG_gdt^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 184 185 ;=============================================================================== 186 SECTION SEG_code32 align=16 187 BITS 32 188 189 mov ax, gsel_flat_data 190 mov ds, ax 191 mov es, ax 192 mov fs, ax 193 mov ss, ax 194 mov esp, TOP_stack 195 196 mov ax, gsel_video 197 mov gs, ax 198 199 PRINT 10, 3, 'P' 200 201 call SetupPaging 202 203 PRINT 10, 4, 'P' 204 205 call InitKernel 206 207 PRINT 10, 5, 'P' 208 209 jmp gsel_flat_code:KERNEL_ENTRY 210 211 ;--------------------------------------------------------------------- 212 213 ;subroutine SetupPaging 214 SetupPaging: 215 216 mov ecx, dword [BASE_mem] 217 test ecx, 00000_00000_11111_11111_11111_11111_11b 218 jnz .another_pde 219 shr ecx, 22 220 jmp .init_pd 221 .another_pde: 222 shr ecx, 22 223 inc ecx 224 .init_pd: 225 push ecx 226 mov edi, BASE_page_dir 227 mov eax, BASE_page_table | PG_U | PG_W | PG_P 228 .1: 229 mov dword [edi], eax 230 add edi, 4 231 add eax, 4 * 1024 232 loop .1 233 .init_pt: 234 mov edi, BASE_page_table 235 mov eax, 0 | PG_U | PG_W | PG_P 236 pop ecx 237 shl ecx, 10 ;ecx *= 1024 238 .2: 239 mov dword [edi], eax 240 add edi, 4 241 add eax, 4 * 1024 242 loop .2 243 244 mov eax, BASE_page_dir 245 mov cr3, eax 246 247 mov eax, cr0 248 or eax, 1 << 31 249 mov cr0, eax 250 251 ret 252 253 ;--------------------------------------------------------------------- 254 255 ;subroutine InitKernel 256 InitKernel: 257 258 xor ebx, ebx 259 mov bx, word [BASE_elf + 0x2a] ;ebx = size of one entry in the pht 260 xor edx, edx 261 mov dx, word [BASE_elf + 0x2c] ;edx = number of entries in the pht 262 mov esi, dword [BASE_elf + 0x1c];esi = offset of the pht 263 add esi, BASE_elf ;esi = address of the pht 264 .loop_edx: 265 test edx, edx 266 jz .endloop_edx 267 cmp dword [esi], 1 268 jne .2 ;the seg can be loaded if type = 1 269 push esi ;backup esi 270 mov edi, dword [esi + 0x8] ;edi = virtual address 271 xor ecx, ecx 272 mov cx, word [esi + 0x10] ;ecx = size 273 mov esi, dword [esi + 0x4] ;esi = offset of program 274 add esi, BASE_elf ;esi = address of program 275 .loop_ecx: 276 test ecx, ecx 277 jz .endloop_ecx 278 mov al, byte [esi] 279 mov byte [edi], al 280 inc esi 281 inc edi 282 dec ecx 283 jmp .loop_ecx 284 .endloop_ecx: 285 pop esi ;restore esi 286 .2: 287 add esi, ebx ;esi += pht_entry_size 288 dec edx 289 jmp .loop_edx 290 .endloop_edx: 291 292 ret 293 294 ;--------------------------------------------------------------------- 295 296 LEN_code32 EQU $ - $$ 297 ;SECTION SEG_code32^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 298 299 ;=============================================================================== 300 SECTION SEG_stack 301 ALIGN 4 302 303 TIMES 2048 DB 0 ;2KB 304 305 TOP_stack EQU BASE_loader + $ 306 ;SECTION SEG_stack^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
loader中要做下面几件事: 1.将elf格式的kernel.bin调入内存。 2.检查内存情况,启动分页机制。 3.切换到保护模式,转移控制到kernel 对1来说,有了boot的经验,这个应该不成问题。 对2来说,我仅仅实现了算出该PC上内存上限,并无考虑内存存在空洞。 对3来说,识别elf格式的地方曾经出了一些问题,因为我用ubuntu下的ld链接出来的elf格式的kernel.bin比书中的代码要多一个program。后来我仔细阅读了elf格式的文档之后修正了这个bug。这个bug是我进入kernel之前的最后一只拦路虎,突破之后就可以开始用C写一些东西了。 最后贴个图,是我在vmware中用软盘启动后的情况:
Feedback
# re: 自制os开发记(一)——启动部分 回复 更多评论
2009-02-06 13:59 by
赞一个。
# re: 自制os开发记(一)——启动部分 回复 更多评论
2009-02-06 16:38 by
现在很少有人这么做了。
|