14 局部段描述符的使用
参考
https://www.cnblogs.com/wanmeishenghuo/tag/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/
https://blog.51cto.com/13475106/category6.html
前面我们使用的都是全局段描述符表,现在我们来分析局部段描述符表的使用。
什么是LDT(Local Descriptor Table)?
局部段描述符表:
本质是一个段描述符表,用于定义段描述符
与GDT类似,可以看做“段描述符的数组”
通过定义选择子访问局部段描述符表中的元素
局部段描述符的选择子和全局描述符的选择子在结构上是完全一样的,3-15位为描述符索引值,LDT选择子的第二位恒为1,1-0位为RPL。
局部段描述符表就是一段内存,里面的每一项是一个局部段描述符(第0项也是有意义的),用于描述一段内存。
CPU中有一个专用寄存器专门指向局部段描述符表。(先定义一个常量 DA_LDT equ 0x82),在全局段描述符表中定义局部段描述符表的描述项时,需要用到属性,这个属性就是DA_LDT。
局部段描述符表的注意事项:
局部段描述符表需要在全局段描述符表中注册(增加描述项)
通过对应的选择子加载局部段描述符(lldt)
局部段描述符表从第0项开始使用(different from GDT)
LDT的意义:
代码层面的意义:
分级管理功能相同意义不同的段(如:多个代码段),全局段描述符表也是有界限的,如果分段过多,则全局段描述符表有可能不够用。而局部段描述符表不限制描述符的个数。引入这种分级管理描述符的机制,可以定义无数个段。
系统层面的意义:
实现多任务的基础要素(每个任务对应一系列不同的段)
LDT的定义与使用:
1、定义独立功能相关的段(代码段、数据段、栈段)
2、将目标段描述符组成局部段描述符表(LDT)
3、为各个段描述符定义选择子(SA_TIL)
4、在GDT中定义LDT的段描述符,并定义选择子
下面给出示例程序,演示局部段描述符的使用。
inc.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 | ; Segment Attribute DA_32 equ 0x4000 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 ; Special Attribute DA_LDT equ 0x82 ; 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 ; 描述符 ; 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 字节 |
我们增加了第12行的宏定义。
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 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 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | %include "inc.asm" org 0x9000 jmp ENTRY_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段属性 GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 CODE16_DESC : Descriptor 0, 0xFFFF, DA_C UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW TASK_A_LDT_DESC : Descriptor 0, TaskALdtLen - 1, DA_LDT ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0 UpdateSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0 TaskALdtSelector equ (0x0007 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] TopOfStack16 equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: DTOS db "D.T.OS!" , 0 DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!" , 0 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ Data32SegLen equ $ - DATA32_SEGMENT [section .s16] [bits 16] ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 mov [BACK_TO_REAL_MODE + 3], ax ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, STACK32_DESC call InitDescItem mov esi, CODE16_SEGMENT mov edi, CODE16_DESC call InitDescItem mov esi, TASK_A_LDT_ENTRY mov edi, TASK_A_LDT_DESC call InitDescItem mov esi, TASK_A_CODE32_SEGMENT mov edi, TASK_A_CODE32_DESC call InitDescItem mov esi, TASK_A_DATA32_SEGMENT mov edi, TASK_A_DATA32_DESC call InitDescItem mov esi, TASK_A_STACK32_SEGMENT mov edi, TASK_A_STACK32_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 ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 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 BACK_ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 in al, 0x92 and al, 11111101b out 0x92, al sti mov bp, HELLO_WORLD mov cx, 12 mov dx, 0 mov ax, 0x1301 mov bx, 0x0007 int 0x10 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 .16] [bits 16] CODE16_SEGMENT: mov ax, UpdateSelector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov eax, cr0 and al, 11111110b mov cr0, eax BACK_TO_REAL_MODE: jmp 0 : BACK_ENTRY_SEGMENT Code16SegLen equ $ - CODE16_SEGMENT [section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, Stack32Selector mov ss, ax mov eax, TopOfStack32 mov esp, eax mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 30 call PrintString mov ax, TaskALdtSelector lldt ax jmp TaskACode32Selector : 0 ;jmp Code16Selector : 0 ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds:ebp] cmp cl, 0 je end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp print end: pop dx pop cx pop edi pop eax pop ebp ret Code32SegLen equ $ - CODE32_SEGMENT [section .gs] [bits 32] STACK32_SEGMENT: times 1014 * 4 db 0 Stack32SegLen equ $ - STACK32_SEGMENT TopOfStack32 equ Stack32SegLen - 1 ; ================================== ; ; Task A Code Segment ; ;=================================== [section .task-a-ldt] ; Task A LDT definition ; 段基址 段界限 段属性 TASK_A_LDT_ENTRY: TASK_A_CODE32_DESC : Descriptor 0, TaskACode32SegLen - 1, DA_C + DA_32 TASK_A_DATA32_DESC : Descriptor 0, TaskAData32SegLen - 1, DA_DR + DA_32 TASK_A_STACK32_DESC : Descriptor 0, TaskAStack32SegLen - 1, DA_DRW + DA_32 TaskALdtLen equ $ - TASK_A_LDT_ENTRY ; Task A LDT Selector TaskACode32Selector equ (0x0000 << 3) + SA_TIL + SA_RPL0 TaskAData32Selector equ (0x0001 << 3) + SA_TIL + SA_RPL0 TaskAStack32Selector equ (0x0002 << 3) + SA_TIL + SA_RPL0 [section .task-a-dat] [bits 32] TASK_A_DATA32_SEGMENT: TASK_A_STRING db "This is Task A" , 0 TASK_STRING_OFFSET equ TASK_A_STRING - $$ TaskAData32SegLen equ $ - TASK_A_DATA32_SEGMENT [section .task-a-gs] [bits 32] TASK_A_STACK32_SEGMENT: times 1024 db 0 TaskAStack32SegLen equ $ - TASK_A_STACK32_SEGMENT TaskATopOfStack32 equ TaskAStack32SegLen - 1 [section .task-a-s32] [bits 32] TASK_A_CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, TaskAStack32Selector mov ss, ax mov eax, TaskATopOfStack32 mov esp, eax mov ax, TaskAData32Selector mov ds, ax jmp $ TaskACode32SegLen equ $ - TASK_A_CODE32_SEGMENT |
从271行开始,我们定义了代表LDT段描述符表本身的段,定义了LDT下的代码段、数据段、栈段。在代码段中,我们加载了相应的段寄存器,最终程序停在原地。
在第17行为LDT段描述符表本身占用的内存增加了段描述符项,用来描述这段内存。
第35行增加了LDT段描述符表所在段的选择子。
218-221行,我们添加了三行程序,作用为加载LDT段描述符表,跳转到LDT段描述符表描述的代码段去执行。
执行结果如下:
我们在LDT描述的代码段中加入以下打印程序:
在324-329行加入了打印字符串的功能,这个PrintString函数是在全局段描述符表的代码段中定义的。
执行结果如下:
我们看到发生了CPU硬件复位。
因为PrintString函数不是属于LDT中的代码段的,我们直接调用它导致了越界,从而CPU复位。因为现在的选择子是LDT段描述符中的选择子,即使PrintString代表的偏移地址是相同的也会发生错误。
我们将打印相关的函数复制一份到LDT描述符下的代码段中,如下:
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 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | %include "inc.asm" org 0x9000 jmp ENTRY_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段属性 GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 CODE16_DESC : Descriptor 0, 0xFFFF, DA_C UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW TASK_A_LDT_DESC : Descriptor 0, TaskALdtLen - 1, DA_LDT ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0 UpdateSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0 TaskALdtSelector equ (0x0007 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] TopOfStack16 equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: DTOS db "D.T.OS!" , 0 DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!" , 0 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ Data32SegLen equ $ - DATA32_SEGMENT [section .s16] [bits 16] ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 mov [BACK_TO_REAL_MODE + 3], ax ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC call InitDescItem mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem mov esi, STACK32_SEGMENT mov edi, STACK32_DESC call InitDescItem mov esi, CODE16_SEGMENT mov edi, CODE16_DESC call InitDescItem mov esi, TASK_A_LDT_ENTRY mov edi, TASK_A_LDT_DESC call InitDescItem mov esi, TASK_A_CODE32_SEGMENT mov edi, TASK_A_CODE32_DESC call InitDescItem mov esi, TASK_A_DATA32_SEGMENT mov edi, TASK_A_DATA32_DESC call InitDescItem mov esi, TASK_A_STACK32_SEGMENT mov edi, TASK_A_STACK32_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 ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 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 BACK_ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16 in al, 0x92 and al, 11111101b out 0x92, al sti mov bp, HELLO_WORLD mov cx, 12 mov dx, 0 mov ax, 0x1301 mov bx, 0x0007 int 0x10 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 .s16] [bits 16] CODE16_SEGMENT: mov ax, UpdateSelector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov eax, cr0 and al, 11111110b mov cr0, eax BACK_TO_REAL_MODE: jmp 0 : BACK_ENTRY_SEGMENT Code16SegLen equ $ - CODE16_SEGMENT [section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, Stack32Selector mov ss, ax mov eax, TopOfStack32 mov esp, eax mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 31 call PrintString mov ax, TaskALdtSelector lldt ax jmp TaskACode32Selector : 0 ; jmp Code16Selector : 0 ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds:ebp] cmp cl, 0 je end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp print end: pop dx pop cx pop edi pop eax pop ebp ret Code32SegLen equ $ - CODE32_SEGMENT [section .gs] [bits 32] STACK32_SEGMENT: times 1024 * 4 db 0 Stack32SegLen equ $ - STACK32_SEGMENT TopOfStack32 equ Stack32SegLen - 1 ; ========================================== ; ; Task A Code Segment ; ; ========================================== [section .task-a-ldt] ; Task A LDT definition ; 段基址, 段界限, 段属性 TASK_A_LDT_ENTRY: TASK_A_CODE32_DESC : Descriptor 0, TaskACode32SegLen - 1, DA_C + DA_32 TASK_A_DATA32_DESC : Descriptor 0, TaskAData32SegLen - 1, DA_DR + DA_32 TASK_A_STACK32_DESC : Descriptor 0, TaskAStack32SegLen - 1, DA_DRW + DA_32 TaskALdtLen equ $ - TASK_A_LDT_ENTRY ; Task A LDT Selector TaskACode32Selector equ (0x0000 << 3) + SA_TIL + SA_RPL0 TaskAData32Selector equ (0x0001 << 3) + SA_TIL + SA_RPL0 TaskAStack32Selector equ (0x0002 << 3) + SA_TIL + SA_RPL0 [section .task-a-dat] [bits 32] TASK_A_DATA32_SEGMENT: TASK_A_STRING db "This is Task A!" , 0 TASK_A_STRING_OFFSET equ TASK_A_STRING - $$ TaskAData32SegLen equ $ - TASK_A_DATA32_SEGMENT [section .task-a-gs] [bits 32] TASK_A_STACK32_SEGMENT: times 1024 db 0 TaskAStack32SegLen equ $ - TASK_A_STACK32_SEGMENT TaskATopOfStack32 equ TaskAStack32SegLen - 1 [section .task-a-s32] [bits 32] TASK_A_CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax mov ax, TaskAStack32Selector mov ss, ax mov eax, TaskATopOfStack32 mov esp, eax mov ax, TaskAData32Selector mov ds, ax mov ebp, TASK_A_STRING_OFFSET mov bx, 0x0C mov dh, 14 mov dl, 29 call TaskAPrintString jmp Code16Selector : 0 ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col TaskAPrintString: push ebp push eax push edi push cx push dx task_print: mov cl, [ds:ebp] cmp cl, 0 je task_end mov eax, 80 mul dh add al, dl shl eax, 1 mov edi, eax mov ah, bl mov al, cl mov [gs:edi], ax inc ebp inc dl jmp task_print task_end: pop dx pop cx pop edi pop eax pop ebp ret TaskACode32SegLen equ $ - TASK_A_CODE32_SEGMENT |
我们复制了打印函数,在第332行跳转到16位的保护模式,进一步跳转到16位实模式。
执行结果如下:
小结:
局部段描述符表用于组织功能相关的段
局部段描述符表需要加载后才能正常使用
局部段描述符表必须在全局段描述符表中注册
通过局部段描述符表的选择子对其进行访问
局部段描述符表时实现多任务的基础
posted on 2020-12-07 10:55 lh03061238 阅读(283) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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)