40 操作系统-从bootloader到内核雏形

参考

https://blog.51cto.com/13475106/category6.html及狄泰软件相关课程

 

一.整体的设计

操作系统-从bootloader到内核雏形
从上图可以得到一个问题,为什么不能从boot直接加载kernnel,并跳转运行?
该设计的思路
1.boot必须小于512字节,无法完成过多功能
2.kernel需要运行于32位保护模式(汇编+c语言)
3.使用loader中转:获取必要硬件信息,进入保护模式
进行的重构方案如下所示
操作系统-从bootloader到内核雏形
文件功能的定义
common.asm--常量定义,宏定义
blfunc.asm--实模式下的文件加载功能定义
boot.asm--加载loader并跳转[引导扇区]
loader.asm--在这里需要进行的操作是必要硬件初始化,加载kernel,进入保护模式,跳转到kernel执行

需要进行的修改
1.将之前定义的inc.asm修改成common.asm,并在makefile中的链接关系将其相关的名字进行修改
2.在loader.asm中相关的头文件进行修改
3.函数重构的实现--将其实现不同的接口[将之前的代码进行改写,细分模块
操作系统-从bootloader到内核雏形
blfunc.asm注意事项
1.%include "blfunc.asm"必须是第一条"包含"语句
2.%include "blfunc.asm"强制从BLMain标签处开始执行
3.Buffer为必要的内存缓冲区必须在代码末尾定义

内核雏形构造
操作系统-从bootloader到内核雏形
通过上图可知,kernel.out加载进内存后不能直接被x86处理器运行,产生的原因有三种
1.kernel.out是Linux系统中的可执行程序
2.Linux中的可执行程序为elf格式的文件-固定数据格式
3.处理器只认代码和数据,无法正确执行elf可执行程序
改进方法
1.提取elf文件中的代码段与数据段-删除elf文件格式信息
2.重定位提取后的代码和数据段,得到内核文件
3.加载内核文到内存-起始地址可自定义
4.跳转到内核入口地址处执行

在这里介绍一个工具,会有三种数据处理格式-如下图所示
操作系统-从bootloader到内核雏形
操作系统-从bootloader到内核雏形
所需的实验文件链接https://down.51cto.com/data/2468702
小结
1.实验nasm和gcc编译得到的是elf目标文件
2.ld将elf目标文件装配成为elf可执行程序
3.实验elf2kobj将可执行程序转换为内核文件
4.在实模式下加载转换得到的内核文件
5.进入保护模式后执行跳转内核起始位置处执行

参考代码分别如下:

bochsrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
###############################################################
# Configuration file for Bochs
###############################################################
 
# how much memory the emulated machine will have
megs: 32
 
# filename of ROM images
romimage: file=/usr/local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/vgabios/vgabios.bin
 
# what disk images will be used
# floppya: 1_44=freedos.img, status=inserted
floppya: 1_44=D.T.OS, status=inserted
 
# choose the boot disk.
boot: a
 
# where do we send log messages?
# log: bochsout.txt
 
# disable the mouse
mouse: enabled=0
 
# enable key mapping, using US layout as default.
keyboard_mapping: enabled=1, map=/usr/local/share/bochs/keymaps/x11-pc-us.map

 

common.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
; PIC-8259A Ports
MASTER_ICW1_PORT                        equ     0x20
MASTER_ICW2_PORT                        equ     0x21
MASTER_ICW3_PORT                        equ     0x21
MASTER_ICW4_PORT                        equ     0x21
MASTER_OCW1_PORT                        equ     0x21
MASTER_OCW2_PORT                        equ     0x20
MASTER_OCW3_PORT                        equ     0x20
 
SLAVE_ICW1_PORT                         equ     0xA0
SLAVE_ICW2_PORT                         equ     0xA1
SLAVE_ICW3_PORT                         equ     0xA1
SLAVE_ICW4_PORT                         equ     0xA1
SLAVE_OCW1_PORT                         equ     0xA1
SLAVE_OCW2_PORT                         equ     0xA0
SLAVE_OCW3_PORT                         equ     0xA0
 
MASTER_EOI_PORT                         equ     0x20
MASTER_IMR_PORT                         equ     0x21
MASTER_IRR_PORT                         equ     0x20
MASTER_ISR_PORT                         equ     0x20
 
SLAVE_EOI_PORT                          equ     0xA0
SLAVE_IMR_PORT                          equ     0xA1
SLAVE_IRR_PORT                          equ     0xA0
SLAVE_ISR_PORT                          equ     0xA0
 
; Segment Attribute
DA_32       equ    0x4000
DA_LIMIT_4K    EQU       0x8000
DA_DR       equ    0x90
DA_DRW      equ    0x92
DA_DRWA     equ    0x93
DA_C        equ    0x98
DA_CR       equ    0x9A
DA_CCO      equ    0x9C
DA_CCOR     equ    0x9E
 
; Segment Privilege
DA_DPL0        equ      0x00    ; DPL = 0
DA_DPL1        equ      0x20    ; DPL = 1
DA_DPL2        equ      0x40    ; DPL = 2
DA_DPL3        equ      0x60    ; DPL = 3
 
; Special Attribute
DA_LDT       equ    0x82
DA_TaskGate  equ    0x85    ; 任务门类型值
DA_386TSS    equ    0x89    ; 可用 386 任务状态段类型值
DA_386CGate  equ    0x8C    ; 386 调用门类型值
DA_386IGate  equ    0x8E    ; 386 中断门类型值
DA_386TGate  equ    0x8F    ; 386 陷阱门类型值
 
; Selector Attribute
SA_RPL0    equ    0
SA_RPL1    equ    1
SA_RPL2    equ    2
SA_RPL3    equ    3
 
SA_TIG    equ    0
SA_TIL    equ    4
 
PG_P    equ    1    ; 页存在属性位
PG_RWR  equ    0    ; R/W 属性位值, 读/执行
PG_RWW  equ    2    ; R/W 属性位值, 读/写/执行
PG_USS  equ    0    ; U/S 属性位值, 系统级
PG_USU  equ    4    ; U/S 属性位值, 用户级
 
; 描述符
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3                              ; 段基址, 段界限, 段属性
    dw    %2 & 0xFFFF                         ; 段界限1
    dw    %1 & 0xFFFF                         ; 段基址1
    db    (%1 >> 16) & 0xFF                   ; 段基址2
    dw    ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0xFF                   ; 段基址3
%endmacro                                     ; 共 8 字节
 
; 门
; usage: Gate Selector, Offset, DCount, Attr
;        Selector:  dw
;        Offset:    dd
;        DCount:    db
;        Attr:      db
%macro Gate 4
    dw    (%2 & 0xFFFF)                      ; 偏移地址1
    dw    %1                                 ; 选择子
    dw    (%3 & 0x1F) | ((%4 << 8) & 0xFF00) ; 属性
    dw    ((%2 >> 16) & 0xFFFF)              ; 偏移地址2
%endmacro

  

blfunc.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
jmp short _start
nop
 
header:
    BS_OEMName     db "D.T.Soft"
    BPB_BytsPerSec dw 512
    BPB_SecPerClus db 1
    BPB_RsvdSecCnt dw 1
    BPB_NumFATs    db 2
    BPB_RootEntCnt dw 224
    BPB_TotSec16   dw 2880
    BPB_Media      db 0xF0
    BPB_FATSz16    dw 9
    BPB_SecPerTrk  dw 18
    BPB_NumHeads   dw 2
    BPB_HiddSec    dd 0
    BPB_TotSec32   dd 0
    BS_DrvNum      db 0
    BS_Reserved1   db 0
    BS_BootSig     db 0x29
    BS_VolID       dd 0
    BS_VolLab      db "D.T.OS-0.01"
    BS_FileSysType db "FAT12   "
     
const:
    RootEntryOffset  equ 19
    RootEntryLength  equ 14
    SPInitValue      equ BaseOfStack - EntryItemLength
    EntryItem        equ SPInitValue
    EntryItemLength  equ 32
    FatEntryOffset   equ 1
    FatEntryLength   equ 9
     
_start:
    jmp BLMain
     
;
; return:
;     dx --> (dx != 0) ? success : failure
LoadTarget:
    mov ax, RootEntryOffset
    mov cx, RootEntryLength
    mov bx, Buffer
     
    call ReadSector
     
    mov si, Target
    mov cx, TarLen
    mov dx, 0
     
    call FindEntry
     
    cmp dx, 0
    jz finish
     
    mov si, bx
    mov di, EntryItem
    mov cx, EntryItemLength
     
    call MemCpy
     
    mov ax, FatEntryLength
    mov cx, [BPB_BytsPerSec]
    mul cx
    mov bx, BaseOfTarget
    sub bx, ax
     
    mov ax, FatEntryOffset
    mov cx, FatEntryLength
     
    call ReadSector
     
    mov dx, [EntryItem + 0x1A]
    mov si, BaseOfTarget / 0x10
    mov es, si
    mov si, 0
     
loading:
    mov ax, dx
    add ax, 31
    mov cx, 1
    push dx
    push bx
    mov bx, si
    call ReadSector
    pop bx
    pop cx
    call FatVec
    cmp dx, 0xFF7
    jnb finish
    add si, 512
    cmp si, 0
    jnz continue
    mov si, es
    add si, 0x1000
    mov es, si
    mov si, 0
continue:
    jmp loading
  
finish:  
    ret
 
; cx --> index
; bx --> fat table address
;
; return:
;     dx --> fat[index]
FatVec:
    push cx
     
    mov ax, cx
    shr ax, 1
     
    mov cx, 3
    mul cx
    mov cx, ax
     
    pop ax
     
    and ax, 1
    jz even
    jmp odd
 
even:    ; FatVec[j] = ( (Fat[i+1] & 0x0F) << 8 ) | Fat[i];
    mov dx, cx
    add dx, 1
    add dx, bx
    mov bp, dx
    mov dl, byte [bp]
    and dl, 0x0F
    shl dx, 8
    add cx, bx
    mov bp, cx
    or  dl, byte [bp]
    jmp return
     
odd:     ; FatVec[j+1] = (Fat[i+2] << 4) | ( (Fat[i+1] >> 4) & 0x0F );
    mov dx, cx
    add dx, 2
    add dx, bx
    mov bp, dx
    mov dl, byte [bp]
    mov dh, 0
    shl dx, 4
    add cx, 1
    add cx, bx
    mov bp, cx
    mov cl, byte [bp]
    shr cl, 4
    and cl, 0x0F
    mov ch, 0
    or  dx, cx
 
return:
    ret
 
; ds:si --> source
; es:di --> destination
; cx    --> length
MemCpy:
     
    cmp si, di
     
    ja btoe
     
    add si, cx
    add di, cx
    dec si
    dec di
     
    jmp etob
     
btoe:
    cmp cx, 0
    jz done
    mov al, [si]
    mov byte [di], al
    inc si
    inc di
    dec cx
    jmp btoe
     
etob:
    cmp cx, 0
    jz done
    mov al, [si]
    mov byte [di], al
    dec si
    dec di
    dec cx
    jmp etob
 
done:  
    ret
 
; es:bx --> root entry offset address
; ds:si --> target string
; cx    --> target length
;
; return:
;     (dx !=0 ) ? exist : noexist
;        exist --> bx is the target entry
FindEntry:
    push cx
     
    mov dx, [BPB_RootEntCnt]
    mov bp, sp
     
find:
    cmp dx, 0
    jz noexist
    mov di, bx
    mov cx, [bp]
    push si
    call MemCmp
    pop si
    cmp cx, 0
    jz exist
    add bx, 32
    dec dx
    jmp find
 
exist:
noexist:
    pop cx
        
    ret
 
; ds:si --> source
; es:di --> destination
; cx    --> length
;
; return:
;        (cx == 0) ? equal : noequal
MemCmp:
 
compare:
    cmp cx, 0
    jz equal
    mov al, [si]
    cmp al, byte [di]
    jz goon
    jmp noequal
goon:
    inc si
    inc di
    dec cx
    jmp compare
     
equal:
noequal:  
 
    ret
 
; es:bp --> string address
; cx    --> string length
Print:
    mov dx, 0
    mov ax, 0x1301
    mov bx, 0x0007
    int 0x10
    ret
 
; no parameter
ResetFloppy:
    push ax
    mov ah, 0x00
    mov dl, [BS_DrvNum]
    int 0x13
    pop ax
    ret
 
; ax    --> logic sector number
; cx    --> number of sector
; es:bx --> target address
ReadSector:
     
    call ResetFloppy
     
    push bx
    push cx
     
    mov bl, [BPB_SecPerTrk]
    div bl
    mov cl, ah
    add cl, 1
    mov ch, al
    shr ch, 1
    mov dh, al
    and dh, 1
    mov dl, [BS_DrvNum]
     
    pop ax
    pop bx
     
    mov ah, 0x02
 
read:   
    int 0x13
    jc read
     
    ret

  

boot.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
BaseOfBoot    equ    0x7C00
 
org BaseOfBoot
 
%include "blfunc.asm"
 
interface:
    BaseOfStack    equ    BaseOfBoot
    BaseOfTarget   equ    0x9000
    Target db  "LOADER     "
    TarLen equ ($-Target)
 
BLMain:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, SPInitValue
     
    call LoadTarget
     
    cmp dx, 0
    jz output
    jmp BaseOfTarget
     
output:
    mov bp, ErrStr
    mov cx, ErrLen
    call Print
     
    jmp $  
 
ErrStr db  "No LOADER" 
ErrLen equ ($-ErrStr)
 
Buffer:
    times 510-($-$$) db 0x00
    db 0x55, 0xaa

  

loader.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
BaseOfLoader   equ   0x9000
 
org BaseOfLoader
 
%include "blfunc.asm"
%include "common.asm"
 
interface:
    BaseOfStack    equ    BaseOfLoader
    BaseOfTarget   equ    0xB000
    Target db  "KERNEL     "
    TarLen equ ($-Target)
 
[section .gdt]
; GDT definition
;                                       Base,         Limit,        Attribute
GDT_ENTRY            :     Descriptor    0,            0,           0
CODE32_FLAT_DESC     :     Descriptor    0,         0xFFFFF,        DA_C + DA_32
CODE32_DESC          :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32
; GDT end
 
GdtLen    equ   $ - GDT_ENTRY
 
GdtPtr:
          dw   GdtLen - 1
          dd   0
           
           
; GDT Selector
Code32FlatSelector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
Code32Selector        equ (0x0002 << 3) + SA_TIG + SA_RPL0
 
 
; end of [section .gdt]
 
 
[section .s16]
[bits 16]
BLMain:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, SPInitValue
     
    ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT
    mov edi, CODE32_DESC
     
    call InitDescItem
     
    ; initialize GDT pointer struct
    mov eax, 0
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY
    mov dword [GdtPtr + 2], eax
     
    call LoadTarget
     
    cmp dx, 0
    jz output
 
    ; 1. load GDT
    lgdt [GdtPtr]
     
    ; 2. close interrupt
    ;    set IOPL to 3
    cli
     
    pushf
    pop eax
     
    or eax, 0x3000
     
    push eax
    popf
     
    ; 3. open A20
    in al, 0x92
    or al, 00000010b
    out 0x92, al
     
    ; 4. enter protect mode
    mov eax, cr0
    or eax, 0x01
    mov cr0, eax
     
    ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0
 
output:
    mov bp, ErrStr
    mov cx, ErrLen
    call Print
     
    jmp $
 
; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:
    push eax
 
    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
     
    pop eax
     
    ret
     
     
[section .s32]
[bits 32]
CODE32_SEGMENT:
    jmp dword Code32FlatSelector : BaseOfTarget
 
Code32SegLen    equ    $ - CODE32_SEGMENT
 
ErrStr db  "No KERNEL" 
ErrLen equ ($-ErrStr)
 
Buffer db  0

  

kentry.asm

1
2
3
4
5
6
7
8
9
10
11
12
global _start
 
extern KMain
 
[section .text]
[bits 32]
_start:
    mov ebp, 0
     
    call KMain
     
    jmp $

 

kmain.c

1
2
3
4
5
6
#include "kernel.h"
 
void KMain()
{
 
}

kernel.h暂时为空文件,无内容

 

makefile修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
.PHONY : all clean rebuild
 
KERNEL_ADDR := B000
IMG := D.T.OS
IMG_PATH := /mnt/hgfs
 
DIR_DEPS := deps
DIR_EXES := exes
DIR_OBJS := objs
 
DIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)
 
KENTRY_SRC := kentry.asm
BLFUNC_SRC := blfunc.asm
BOOT_SRC   := boot.asm
LOADER_SRC := loader.asm
COMMON_SRC := common.asm
 
KERNEL_SRC := kmain.c
 
BOOT_OUT   := boot
LOADER_OUT := loader
KERNEL_OUT := kernel
KENTRY_OUT := $(DIR_OBJS)/kentry.o
 
EXE := kernel.out
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
 
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
 
all : $(DIR_OBJS) $(DIR_EXES) $(IMG) $(BOOT_OUT) $(LOADER_OUT) $(KERNEL_OUT)
    @echo "Build Success ==> D.T.OS!"
     
ifeq ("$(MAKECMDGOALS)", "all")
-include $(DEPS)
endif
 
ifeq ("$(MAKECMDGOALS)", "")
-include $(DEPS)
endif
 
$(IMG) :
    bximage $@ -q -fd -size=1.44
     
$(BOOT_OUT) : $(BOOT_SRC) $(BLFUNC_SRC)
    nasm $< -o $@
    dd if=$@ of=$(IMG) bs=512 count=1 conv=notrunc
     
$(LOADER_OUT) : $(LOADER_SRC) $(COMMON_SRC) $(BLFUNC_SRC)
    nasm $< -o $@
    sudo mount -o loop $(IMG) $(IMG_PATH)
    sudo cp $@ $(IMG_PATH)/$@
    sudo umount $(IMG_PATH)
     
$(KENTRY_OUT) : $(KENTRY_SRC) $(COMMON_SRC)
    nasm -f elf $< -o $@
     
$(KERNEL_OUT) : $(EXE)
    ./elf2kobj -c$(KERNEL_ADDR) $< $@
    sudo mount -o loop $(IMG) $(IMG_PATH)
    sudo cp $@ $(IMG_PATH)/$@
    sudo umount $(IMG_PATH)
     
$(EXE) : $(KENTRY_OUT) $(OBJS)
    ld -s $^ -o $@
     
$(DIR_OBJS)/%.o : %.c
    gcc -fno-builtin -fno-stack-protector -o $@ -c $(filter %.c, $^)
 
$(DIRS) :
    mkdir $@
 
ifeq ("$(wildcard $(DIR_DEPS))", "")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
    @echo "Creating $@ ..."
    @set -e; \
    gcc -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@ : ,g' > $@
     
clean :
    rm -fr $(IMG) $(BOOT_OUT) $(LOADER_OUT) $(KERNEL_OUT) $(DIRS)
     
rebuild :
    @$(MAKE) clean
    @$(MAKE) all

  

  

  

 

 

 

 

posted on   lh03061238  阅读(244)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2020-04-04 08.泛型编程简介
2020-04-04 C语言中的指针和内存泄漏

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示