第三天 - 32位模式与C语言
32位模式与C语言
一、32位模式
1. 制作IPL(Initial Program Loader)
-
启动区 - 磁盘最初的512个字节是启动区
-
IPL - 启动程序装载器,装在启动区内,用于加载真正的操作系统
汇编代码解析:
; ipl.nas
; hello-os
; TAB = 4
ORG 0x7c00 ; 指明程序的装载地址
; 以下的记述用于标准FAT12格式的软盘
JMP entry
DB 0x90
DB "HARIBOTE" ; 启动区的名称可是任意的字符串
DW 512 ; 每个扇区(sector)的大小(必须为512字节)
DB 1 ; 簇(cluster)的大小(必须为1个扇区)
DW 1 ; FAT12的起始位置(一般从第一个扇区开始)
DB 2 ; FAT的个数(必须为2)
DW 224 ; 根目录的大小(一般设成224项)
DW 2880 ; 该磁盘的大小(必须是2880扇区)
DB 0xf0 ; 磁盘的种类(必须是0xf0)
DW 9 ; FAT的长度(必须是9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须是18)
DW 2 ; 磁头数(必须是2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明固定
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; 磁盘的名称(11字节)
DB "FAT12 " ; 磁盘格式名称
RESB 18 ; 先空出18字节
; 程序主体
entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
; 读光盘
MOV AX,0X0820
MOV ES,AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
MOV AH,0x02 ; AH=0x02 : 读盘
MOV AL,1 ; 1个扇区
MOV EX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JC error
; 虽然读完了,但是因为暂时没有要做的事所以停止等待指令
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环
error:
MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
msg:
DB 0x0a, 0x0a ; 换行两次
DB "load error"
DB 0x0a
DB 0
RESB 0x7dfe-$
DB 0x55, 0xaa
读光盘代码分析 - 把第二扇区的内容加载到 ox0820的内存地址
- 0x7c00 ~ 0x7dff 这一个扇区(512个字节)用于存储启动区代码
- 之所以加载到0x08000的位置,指示因为 0x7e00 ~ 0x9fbff 的内存暂无用途,所以使用空闲的0x8000
MOV AX,0x0820
MOV ES, AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
MOV AH,0x02 ; AH=0x02 : 读盘
MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JC error
- JC指令
- jump if carry
- 如果进位标志为1 (carry flag) 则跳转
- INT 0x13中断命令
BIOS 磁盘读写内容
;磁盘读写,扇区校验,寻道
AH = 0x02 #读盘
AH = 0x03 #写盘
AH = 0x04 #校验
AH = 0x0c # 寻道
AL = 处理对象的扇区数(连续的扇区)
CH = 柱面号
CL = 扇区号
DH = 磁头号
DL = 驱动号
ES:BX = 缓冲地址 (校验及寻道时不使用)
返回值:
FLACS.CF = 0 : 没有错误 AH = 0
FLACS.CF = 1:有错误,错误值写入AH(与重置(reset)功能一样)
-
FLACS.CF 进位标志
- 调用这个读盘函数后,如果没错,进位标志位0,否则为1,所以使用JC指令
-
磁头
-
扇区
- 一个扇区512个字节
-
含有IPL的启动区 是 柱面0,磁头0,扇区1 ( C0-H0S1),下一个要装载的扇区是C0-H0-S2
-
缓冲区地址
- 要将软盘的数据装载到内存中的位置
- BX只能表示64KB的内存,SI是辅助寄存区,指定一个ES x 16 + BX 的内存地址
-
但我们要指定内存的地址时,必须同时指定段寄存器,一般把ES作为段寄存器
- 写MOV CX, [1234] == MOV CX, [ES:1234]
- 所以做好ES预先设置为0
-
进位标志
- flag,常用于报告BIOS函数调用是否有错误,进位为0则正确,进位为1则错误
2. 读盘试错代码分析
;读磁盘
MOV AX,0x0820
MOV ES, AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL,1 ; 要处理的扇区数为1
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JNC fin ; 没出错的话跳转到fin
ADD SI,1 ; 往SI加1
CMP SI,5 ; 比较SI与5
JAE error ; SI >= 5时,跳转到error
MOV AH,0x00
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
- JNC命令
- 跳转指令 - Jump if not carry
- 进位为0则跳转
- JAE
- jump if above or equal
- 大于或等于则跳转
- 在读盘出错后,查看是否超过错误次数,没有则调用AH,DL指令,操作进行系统复位,恢复软盘的状态,然后跳转到 retry: 标号处再次读盘
3. 读18扇区的代码
; 读磁盘
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
readloop:
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 读盘
MOV AL,1 ; 每次读取1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动
INT 0x13 ; BIOS
JNC next ; 没出错则跳转到 next
ADD SI,1 ; SI +1
CMP SI,5 ; SI与5比較
JAE error ; SI >= 5 跳转到error
MOV AH,0x00
MOV DL,0x00 ;A驱动
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX,ES ; 把内存地址后移一个扇区大小0x200
ADD AX,0x0020
MOV ES,AX ; ADD ES,0x020
ADD CL,1 ; CL + 1
CMP CL,18 ; 比较18
JBE readloop ; CL <= 18 跳转到readloop
-
JBE指令
- jump if below or equal
- 小于等于则跳转
-
next 操作
- 读下一个扇区 CL+1
- ES指定读入地址,加上512字节,512/16 == 0x20
-
上述操作结束后,我们已经把18个磁盘的内容装载到0x8200 - 0xa3ff
4. 读入10个柱面
- 读取柱面c0-H0-s1 到 C9-H1-S18
; 读磁盘
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; 柱面 0
MOV DH,0 ; 磁头 0
MOV CL,2 ; 扇区 2
readloop:
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL,1 ; 1 个扇区
MOV BX,0
MOV DL,0x00 ; A 驱动器
INT 0x13 ; 调用磁盘BIOS
JNC next ; 没出错时跳转到next
ADD SI,1 ; SI 加 1
CMP SI,5 ; 比较 SI 与 5
JAE error ; SI >= 5 时,跳转到error
MOV AH,0x00
MOV DL,0x00 ; A 驱动器
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX,ES ; 把内存地址后移0x200
ADD AX,0x0020
MOV ES,AX ; 因为没有 ADD ES,0x020 指令,所以这里稍微绕个弯
ADD CL,1 ; CL 加 1
CMP CL,18 ; 比较 CL 与 18
JBE readloop ; 如果 CL <= 18,则跳转至readloop
MOV CL,1
ADD DH,1 ; 磁头+1
CMP DH,2 ; 判断磁头数量
JB readloop ; 上下两个磁头,如果 DH < 2, 则跳转到readloop
MOV DH,0
ADD CH,1
CMP CH,CYLS ; 判断柱面数量
JB readloop ; 如果 CH < CYLS,则跳转至readloop
-
JB指令
- jump if below
- 小于则跳转
-
EQU指令
- 相当于#define,用于声明常数,前面定义了CYLS
- 声明CYLS为常数10
-
这个程序就从软盘读取10个柱面 即 10 * 2 * 18 * 512, = 180KB内容装载到内存中,填满了0x8200 = 0x34fff
5. 开发操作系统
-
前面完成了启动区的制作,编写一个简单的程序
-
;haribote.nas 汇编文件 fin: HLT JMP fin
-
-
将文件保存到磁盘映像的操作
-
make 指令 先将磁盘映像文件写入磁盘
-
make img
-
0x002600附近,磁盘的这个位置保存着文件名:haribote.sys,
-
0x004200那里,可以看到“F4 EB FD”
-
查看二进制代码
-
-
在windows文件系统找到磁盘,将haribote.sys保存到磁盘
-
使用工具将磁盘备份作为磁盘映像
-
总结
-
一般向一个空软盘中保存文件时
-
文件名写在0x002600以后的地方
-
文件的内容写在0x004200以后的地方
-
-
下一步工作
- 就是将系统本身的内容写到名为haribote.sys文件中,将这个文件保存到磁盘映像中,然后在启动区执行这个文件
6,从启动区执行操作系统
-
如何执行磁盘映像上,位于地址是0x004200中程序?
-
程序从启动区开始,把磁盘的内容 (布娃娃系统的代码) 装载到内存地址0x8000处
-
该文件内容在磁盘中地址是0x00420,那么在该haribote.sys文件内容在内存地址 0x8000+0x4200=0xc200
-
给haribote.sys代码加上 org 0xc200,给 ipl.nas 加上 jmp 0xc200
-
haribote.nas 汇编文件 代码如下:
-
;haribote.nas 汇编文件 ; haribote-os ; TAB=4 ORG 0xc200 ; 指示这个程序会装载到什么地方 MOV AL,0x13 ; VGA显卡,320x200x8位彩色 MOV AH,0x00 INT 0x10 fin: HLT JMP fin
-
-
结果暂无输出
7. 调用BIOS中断,切换显示模式,实现显示全黑
-
设置显卡模式
-
; haribote-os ; TAB=4 ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢? MOV AL,0x13 ; VGA显卡,320x200x8位彩色 MOV AH,0x00 ; 功能00h,设置显示器模式 INT 0x10 fin: HLT JMP fin
-
AH 设置为 Ox00
-
-
阶段运行
haribote.nas 文件
; haribote-os
; TAB=4
ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢?
MOV AL,0x13 ; VGA显卡,320x200x8位彩色
MOV AH,0x00 ; 功能00h,设置显示器模式
INT 0x10
fin:
HLT
JMP fin
ipl.nas 启动区文件
; haribote-ipl
; TAB=4
CYLS EQU 10 ; 要读取到什么程度
ORG 0x7c00 ; 启动装载程序
; 以下记述用于标准FAT12格式软盘
JMP entry
DB 0x90
DB "HARIBOTE" ; 磁盘名称(可以是任意字符串)
DW 512 ; 每个扇区的大小(必须是512)
DB 1 ; 簇的大小(必须为一个扇区)
DW 1 ; FAT12的起始位置(一般从第一个扇区开始
DB 2 ; FAT的个数(必须为2)
DW 224 ; 根目录的大小(一般设成224项)
DW 2880 ; 该磁盘的大小(必须是2880扇区)
DB 0xf0 ; 该磁盘的种类(必须是0xf0
DW 9 ; FAt的长度(必须是9扇区)
DW 18 ; 一个磁道有几个扇区(必须是18)
DW 2 ; 磁头数(必须是2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 磁盘大小
DB 0,0,0x29 ; 意义不明固定
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; 磁盘的名称(11字节)
DB "FAT12 " ; 磁盘格式名称(8字节)
RESB 18 ; 先空出18字节
; 程序主体
entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
; 读磁盘
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; 柱面 0
MOV DH,0 ; 磁头 0
MOV CL,2 ; 扇区 2
readloop:
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL,1 ; 1 个扇区
MOV BX,0
MOV DL,0x00 ; A 驱动器
INT 0x13 ; 调用磁盘BIOS
JNC next ; 没出错时跳转到next
ADD SI,1 ; SI 加 1
CMP SI,5 ; 比较 SI 与 5
JAE error ; SI >= 5 时,跳转到error
MOV AH,0x00
MOV DL,0x00 ; A 驱动器
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX,ES ; 把内存地址后移0x200
ADD AX,0x0020
MOV ES,AX ; 因为没有 ADD ES,0x020 指令,所以这里稍微绕个弯
ADD CL,1 ; CL 加 1
CMP CL,18 ; 比较 CL 与 18
JBE readloop ; 如果 CL <= 18,则跳转至readloop
MOV CL,1
ADD DH,1
CMP DH,2
JB readloop ; 如果 DH < 2, 则跳转到readloop
MOV DH,0
ADD CH,1
CMP CH,CYLS
JB readloop ; 如果 CH < CYLS,则跳转至readloop
; 因为看完了实行haribote.sys
MOV [0x0ff0],CH ; IPL读到什么地方结束
JMP 0xc200
error:
MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI,1 ; SI 加 1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPu停止,等待指令
JMP fin ; 无限循环
msg:
DB 0x0a, 0x0a ; 换行两次
DB "load error"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 重复0x00一直到0x7dfe
DB 0x55, 0xaa
- 为了把磁盘装载内容的结束地址告诉 haribote.sys, 在 JMP 0xc200之前,把 CYLS常量的值写入内存地址 0x0ff0
Makefile文件
TOOLPATH = ../z_tools/
MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del
# 默认
default :
$(MAKE) img
# 文件生成规则
ipl10.bin : ipl10.nas Makefile
$(NASK) ipl10.nas ipl10.bin ipl10.lst
haribote.sys : haribote.nas Makefile
$(NASK) haribote.nas haribote.sys haribote.lst
haribote.img : ipl10.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
imgout:haribote.img
# 命令
img :
$(MAKE) haribote.img
run :
$(MAKE) img
$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu
install :
$(MAKE) img
$(IMGTOL) w a: haribote.img
clean :
-$(DEL) ipl10.bin
-$(DEL) ipl10.lst
-$(DEL) haribote.sys
-$(DEL) haribote.lst
src_only :
$(MAKE) clean
-$(DEL) haribote.img
编译 和 运行
-
双击 !cons_nt.bat,并在打开的命令行中输入 make run
-
效果:画面一片黑色
8. 为32位模式做准备
-
BIOS功能是用16位机器语言写的,在32位模式下不能用,需要BIOS功能做的事情放在开头完成
- 如获取键盘状态(数字小键盘 NumLock是ON还是OFF等功能)
-
32位模式的优点
- 可以使用的内存容量远远大于1MB
- CPU的自我保护功能(识别出可疑的机器语言并进行屏蔽,以免破坏系统)在16位下不能用,但32位下能用
-
在设置完画面模式后,需要将画面模式的信息保存在内存中,便于后续支持各种不同的画面模式时使用
-
; haribote-os ; TAB=4 ; 有关BOOT_INFO CYLS EQU 0x0ff0 ; 设定启动区 LEDS EQU 0x0ff1 VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色的位数。 SCRNX EQU 0x0ff4 ; 分辨率的X(screen x) SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y) VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址 ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢? MOV AL,0x13 ; VGA显卡,320x200x8位彩色 MOV AH,0x00 INT 0x10 MOV BYTE [VMODE],8 ; 记录画面模式 MOV WORD [SCRNX],320 MOV WORD [SCRNY],200 MOV DWORD [VRAM],0x000a0000 ;用BIOS取得键盘上各种LED指示灯的状态 MOV AH,0x02 INT 0x16 ; keyboard BIOS MOV [LEDS], AL fin: HLT JMP fin
-
-
[VRAM]内存地址保存的是显卡内存(video RAM),用于显示画面的内存,保存起来备用
- 显存在内存也分了几个位置,不同位置的显存用于不同的画图模式,显存的内存地址在BIOS中断函数中有说明
9. 导入C语言
- 开始由汇编转入C语言,并提供了汇编代码去调用C语言代码(后续讲解)
- haribote.sys 改名位 asmhead.nas, 前半部分用汇编,后半部分用C语言编写
asmhead.nas 文件
; haribote-os boot asm
; TAB=4
BOTPAK EQU 0x00280000 ; bootpack装载处
DSKCAC EQU 0x00100000 ; 磁盘缓存的地方
DSKCAC0 EQU 0x00008000 ; 磁盘高速缓存的场所(实时模式)
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息,颜色的位数
SCRNX EQU 0x0ff4 ; 分辨率的X(screen x)
SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y)
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 这个程序要被装载到内存的什么地方呢?
; 画面模式设定
MOV AL,0x13 ; VGA显卡,320x200x8位彩色
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 记录画面模式(C语言参照)
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
; 用BIOS取得键盘各种LED指示灯的状态
MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL
; 使PIC不授受一切中断
; 如果要初始PIC的话,要在AT兼容的规范中,
; 如果不把这家伙放在CLI面前,我偶尔会举起来
; 稍后进行PIC的初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果连续OUT命令的话,可能会有不太好的机型
OUT 0xa1,AL
CLI ; 甚至禁止CPU层面插队
; cpu从1 mb以上的内存,a20gate设定
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
; 保护模式过渡
[INSTRSET "i486p"] ; 用于记述想要使用的486命令
LGDT [GDTR0] ; 暂定GDT设定
MOV EAX,CR0
AND EAX,0x7fffffff ; 使bit31为0(为子禁止寻乎)
OR EAX,0x00000001 ; 使bit0为1(为子保护模式转移)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 可读区段32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
; bootpack的传送
MOV ESI,bootpack ; 传输源
MOV EDI,BOTPAK ; 传输目的地
MOV ECX,512*1024/4
CALL memcpy
; 顺便磁盘数据也向原来的位置传送
; 首先从引导扇区
MOV ESI,0x7c00 ; 传输源
MOV EDI,DSKCAC ; 传输目的地
MOV ECX,512/4
CALL memcpy
; 剩下的全部
MOV ESI,DSKCAC0+512 ; 传输源
MOV EDI,DSKCAC+512 ; 传输目的地
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 从柱面数转换成字节数 /4
SUB ECX,512/4 ; 通过IPL减去
CALL memcpy
; 我们已经完成了需要使用asmhead 进行的所有操作
; 放到bootpack中
; 启动bootpack
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 无需转移
MOV ESI,[EBX+20] ; 传输源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 传输目的地
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 堆栈初始值
JMP DWORD 2*8:0x0000001b
waitkbdout:
IN AL,0x64
AND AL,0x02
JNZ waitkbdout ; 如果AND的结果不为0,请跳转至waitkbdou
RET
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 如果减法不为0,则返回memcpy
RET
; memcpy也可以用字符串指令编写,除非你忘记了地址大小写前缀
ALIGNB 16
GDT0:
RESB 8 ; 空选择器
DW 0xffff,0x0000,0x9200,0x00cf ; 读/定段32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可执行段32bit(用于bootpack)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
10 . 实现HLT
汇编程序:naskfunc.nas
; naskfunc
; TAB=4
[FORMAT "WCOFF"] ; 制作目标文件的模式
[BITS 32] ; 制作32位模式用的机器语言
; 制作目标文件的信息
[FILE "naskfunc.nas"] ; 源文件名信息
GLOBAL _io_hlt ; 程序中包含的函数名
; 以下是实际的函数
[SECTION .text] ; 目标文件中写了这些之后再写程序
_io_hlt: ; void io_hlt(void)
HLT
RET
- 代码解析
- 汇编先声明一个与函数名相同的标号,然后开始写代码
- RET命令
- 相当于 return,表示函数结束,返回
- 汇编函数前需要添加下划线,用于与C语言函数链接
bootpack.c 文件 - C语言程序使用汇编程序的函数
-
/* 告诉 C 编译器,有一个函数在别的文件里 */ void io_hlt(void); /* 是函数声明却不用{},而用;,这表示的意思是:函数在别的文件中,你自己找一下吧! */ void HariMain(void) { fin: io_hlt(); /* 执行naskfunc.nas里的_io_hlt */ goto fin; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现