程序员的网络日志 程序员的网络日志
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理
  • C语言
  • 数据结构
  • 汇编语言
  • 操作系统
  • Linux
  • 编译原理
  • 计算机网络
  • 数据库
  • Java基础
  • JavaWeb
  • 笔记软件
  • Quicker
  • Qttabar
  • Wgesture
  • 浏览器与插件
  • 视频播放器
  • 待办清单
  • 终端软件
  • uTools
  • 番茄盒子
  • 网站日记
  • 赞赏支持
  • 关于本站
  • 如何搭建一个博客
  • 如何搭建一个邮箱
GitHub (opens new window)
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理
  • C语言
  • 数据结构
  • 汇编语言
  • 操作系统
  • Linux
  • 编译原理
  • 计算机网络
  • 数据库
  • Java基础
  • JavaWeb
  • 笔记软件
  • Quicker
  • Qttabar
  • Wgesture
  • 浏览器与插件
  • 视频播放器
  • 待办清单
  • 终端软件
  • uTools
  • 番茄盒子
  • 网站日记
  • 赞赏支持
  • 关于本站
  • 如何搭建一个博客
  • 如何搭建一个邮箱
GitHub (opens new window)
  • 计算机科学导论

  • 操作系统

    • 我的操作系统学习笔记

    • 我的操作系统实验笔记

      • 搭建实验环境
      • 操作系统的引导
        • 实验介绍
        • 改写bootsect.s
        • 2. 改写setup.s
        • 扩展
        • 附录
  • Linux

  • 编译原理

  • 计算机网络

  • 数据库

  • 计算机科学导论
  • 计算机基础
  • 操作系统
  • 我的操作系统实验笔记
2022-10-30
目录

操作系统的引导

# 1. 操作系统的引导

# 实验介绍

此次实验的基本内容是:

  1. 阅读《Linux内核完全注释》的第6章,对计算机和Linux 0.11的引导过程进行初步的了解;
  2. 按照下面的要求改写0.11的引导程序bootsect.s
  3. 有兴趣同学可以做做进入保护模式前的设置程序setup.s。

‍

‍

​

在实验报告中回答如下问题:

  1. 有时,继承传统意味着别手蹩脚。x86计算机为了向下兼容,导致启动过程比较复杂。请找出x86计算机启动过程中,被硬件强制,软件必须遵守的两个“多此一举”的步骤(多找几个也无妨),说说它们为什么多此一举,并设计更简洁的替代方案。 评分标准
  2. bootsect显示正确,30%
  3. bootsect正确读入setup,10%
  4. setup获取硬件参数正确,20%
  5. setup正确显示硬件参数,20%
  6. 实验报告,20%

‍

‍

‍

# 改写bootsect.s

bootsect.s能在屏幕上打印一段提示信息“XXX is booting...”,其中XXX是你给自己的操作系统起的名字,例如LZJos、Sunix等(可以上论坛上秀秀谁的OS名字最帅,也可以显示一个特色logo,以表示自己操作系统的与众不同。)

‍

# 简单版v1.0

简单的话,我们直接改版原本的bootsect.s 的字符串和字符串个数即可,步骤如下:

cd ~/oslab/linux-0.11/boot
vim bootsect.s

‍

修改msg1的内容,同时修改cx的值

修改246的字符串为 "POS is runing"
第98行 mov cx,#23

然后保存并退出vim

‍

重新编译

as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
dd bs=1 if=bootsect of=Image skip=32

我们可以写个shell脚本,这样以后每次编译的时候,就不用写上述命令了,我这里新建一个lab1shell.sh,里面放上面3条命令

‍

‍

生成内核

cd ~/oslab/linux-0.11
make all

‍

可以看到Image的时间已经是被更新为你编译的时候的时间

$ ll
-rw-rw-r--  1 peterjxl peterjxl 128161 10月 28 07:54 Image

‍

运行Bochs

cd ~/oslab
./run

‍

实验截图

​

‍

# 不太简单版v2.0

接下来我们重写bootsect.s ,只完成关键的功能即可

‍

‍

首先完成屏幕输出功能:屏幕输出的功能主要是用10号中断

! 首先读入光标位置
mov ah, #0x03 
xor bh, bh
int 0x10 

输入参数:bh是页号,这里置0

返回参数:CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based)

‍

‍

然后定义字符串:

msg1:	.byte 13, 10
	.ascii "Hello OS World, my name is JXL"
	.byte 13, 10, 13, 10

.org 510

boot_flag: .word 0xAA55

.byte 13,10就是定义个回车和换行,第4行就是两个回车和换行

.org 510 伪指令,表示在它之后的指令从地址510开始存放。遇到.org,编译器会把其后的指令代码放到org伪指令指定的偏移地址。如org指定的地址和之前的指令地址有空洞,则用0填充。

boot_flag: .word 0xAA55:是启动盘具有有效引导扇区的标志。仅供BI0S中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中。

‍

‍

然后是显示字符串

! 显示字符串
mov cx, #36
mov bx, #0x0007
mov ax, #0x7c00
mov es, ax
mov bp, #msg1
mov ax, #0x1301
int 0x10

调用参数:cx是字符串长度,BH=页号,BL=显示属性,,es:bp是字符串的地址,ax是功能号 显示字符串光标跟随移动

‍

‍

显示完字符串后,我们不再往下执行(例如载入setup.s 和把自己挪到0x9000处),我们这里可以写个简单的死循环

inf_loop:
	jmp inf_loop

‍

‍

完整代码如下:

entry _start
_start:
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#36
    mov bx,#0x0007
    mov bp,#msg1
    mov ax,#0x07c0
    mov es,ax
    mov ax,#0x1301
    int 0x10
inf_loop:
    jmp inf_loop

msg1: 	.byte   13,10
    	.ascii  "Hello OS world, my name is LZJ"
    	.byte   13,10,13,10

.org 510

boot_flag: .word  0xAA55

‍

‍

我们重新编译和运行,实验结果如下

​

运行后,就不会继续往下有输出了,因为是死循环。

‍

‍

‍

# 2. 改写setup.s

改写setup.s主要完成如下功能:

  1. bootsect.s能完成setup.s的载入,并跳转到setup.s开始地址执行。而setup.s向屏幕输出一行"Now we are in SETUP"。
  2. setup.s能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
  3. setup.s不再加载Linux内核,保持上述信息显示在屏幕上即可。

‍

‍

# 加载setup.s版 v1.0

同样是显示字符串,因此可以直接复用 bootsect.s的2.0版本,改改显示的字符串即可。我们可以直接删掉setup.s,然后cp一份bootsect.s

$ cat bootsect.s 
$ cp bootsect.s setup.s

然后有4个地方要修改:

​

‍

‍

完整代码:

entry _start
_start:
    mov ah,#0x03
    xor bh,bh
    int 0x10

    mov cx,#25
    mov bx,#0x0007
    mov bp,#msg2
    mov ax,cs
    mov es,ax
    mov ax,#0x1301
    int 0x10
inf_loop:
    jmp inf_loop

msg2: 	.byte   13,10
    	.ascii  "Now we are in SETUP"
    	.byte   13,10,13,10

.org 510

boot_flag: .word  0xAA55

‍

‍



修改bootsect.s 加载setup模块 v3.0,我们可以参考原版的bootsect.s的模块,我们只需要将其copy到我们自己的bootsect.s即可……

load_setup:
! 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
    mov dx,#0x0000
! 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
    mov cx,#0x0002
! 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
    mov bx,#0x0200
! 设置读入的扇区个数(service 2, nr of sectors),
! SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,
! 我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
    mov ax,#0x0200+SETUPLEN
! 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
    int 0x13
! 读入成功,跳转到 ok_load_setup: ok - continue
    jnc ok_load_setup
! 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
    mov dx,#0x0000
! 否则复位软驱 reset the diskette
    mov ax,#0x0000
    int 0x13
! 重新循环,再次尝试读取
    jmp load_setup
ok_load_setup:
! 接下来要干什么?当然是跳到 setup 执行。
! 要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0
! 即我们要设置 SETUPSEG=0x07e0

‍

完整的bootsect.s代码如下:

SETUPLEN=2
SETUPSEG=0x07e0
entry _start
_start:
    mov ah,#0x03
    xor bh,bh
    int 0x10

    mov cx,#36
    mov bx,#0x0007
    mov bp,#msg1
    mov ax,#0x07c0
    mov es,ax
    mov ax,#0x1301
    int 0x10
load_setup:
    mov dx, #0x0000 ; 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
    mov cx, #0x0002 ; 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
    mov bx, #0x0200 ; 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
    mov ax, #0x0200 + SETUPLEN; 设置读入的扇区个数(service 2, nr of sectors),SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
    int 0x13         ; 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
    jnc ok_load_setup ; 读入成功,跳转到 ok_load_setup: ok - continue

    mov dx, #0x0000 ; 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
    mov ax, #0x0000 ; 复位软驱 reset the diskette 
    int 0x13    ; 继续尝试加载setup.s模块
    jmp load_setup


; 接下来要干什么?当然是跳到 setup 执行。要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0, 即我们要设置 SETUPSEG=0x07e0  
ok_load_setup:
    jmpi 0, SETUPSEG


msg1: 	.byte   13,10
    	.ascii  "Hello OS world, my name is JXL"
    	.byte   13,10,13,10

.org 510

boot_flag: .word  0xAA55

‍

然后我们再次编译,setup.s和bootsect.s都要编译和链接,效率较低,类似我只之前讲到的写个shell脚本来编译,这次我们不用自己写,用Makefile里自带的即可

$ cd ~/oslab/linux-0.11/

$ make BootImage 
as86 -0 -a -o boot/bootsect.o boot/bootsect.s
ld86 -0 -s -o boot/bootsect boot/bootsect.o
as86 -0 -a -o boot/setup.o boot/setup.s
ld86 -0 -s -o boot/setup boot/setup.o
tools/build boot/bootsect boot/setup none  > Image
Root device is (3, 1)
Boot sector 512 bytes.
Setup is 512 bytes.
Unable to open 'system'
make: *** [Makefile:54:BootImage] 错误 1

有 Error!这是因为 make 根据 Makefile 的指引执行了 tools/build.c​,它是为生成整个内核的镜像文件而设计的,没考虑我们只需要 bootsect.s​ 和 setup.s​ 的情况。它在向我们要 “系统” 的核心代码。为完成实验,接下来给它打个小补丁。

‍

我们可以简单的看看 linux-0.11/Makefile的内容:

all:	Image

Image: boot/bootsect boot/setup tools/system tools/build
	cp -f tools/system system.tmp
	strip system.tmp
	objcopy -O binary -R .note -R .comment system.tmp tools/kernel
	tools/build boot/bootsect boot/setup tools/kernel $(ROOT_DEV) > Image
	rm system.tmp
	rm tools/kernel -f
	sync

BootImage: boot/bootsect boot/setup tools/build
	tools/build boot/bootsect boot/setup none $(ROOT_DEV) > Image
	sync

​build.c​ 从命令行参数得到 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image。其中 system 是第三个参数(argv[3])。当 “make all” 的时候,这个参数传过来的是正确的文件名,build.c​ 会打开它,将内容写入 Image。

而 “make BootImage” 时,传过来的是字符串 "none"。所以,改造 build.c 的思路就是当 argv[3] 是"none"的时候,只写 bootsect 和 setup,忽略所有与 system 有关的工作,或者在该写 system 的位置都写上 “0”。

修改工作主要集中在 build.c​ 的尾部,可以参考下面的方式,将圈起来的部分注释掉(也可以删掉)。

​

‍

‍

当按照前一节所讲的编译方法编译成功后再 run,实验结果如下图

$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../run

‍

​

‍

‍

‍

‍

‍

‍

# 获取硬件参数版 v2.0

在理论课里我们讲到过,setup.s 将获得硬件参数放在内存的 0x90000 处。

用 ah=#0x03​ 调用 0x10​ 中断可以读出光标的位置,用 ah=#0x88​ 调用 0x15​ 中断可以读出内存的大小。有些硬件参数的获取要稍微复杂一些,如磁盘参数表。在 PC 机中 BIOS 设定的中断向量表中 int 0x41​ 的中断向量位置(4*0x41 = 0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。第二个硬盘的基本参数表入口地址存于 int 0x46​ 中断向量位置处。每个硬盘参数表有 16 个字节大小。下表给出了硬盘基本参数表的内容:

表 1 磁盘基本参数表

位移 大小 说明
0x00 字 柱面数
0x02 字节 磁头数
... ... ...
0x0E 字节 每磁道扇区数
0x0F 字节 保留

所以获得磁盘参数的方法就是复制数据。

下面是将硬件参数取出来放在内存 0x90000 的关键代码。

mov    ax,#INITSEG
! 设置 ds = 0x9000
mov    ds,ax
mov    ah,#0x03
! 读入光标位置
xor    bh,bh
! 调用 0x10 中断
int    0x10
! 将光标位置写入 0x90000.
mov    [0],dx

! 读入内存大小位置
mov    ah,#0x88
int    0x15
mov    [2],ax

! 从 0x41 处拷贝 16 个字节(磁盘参数表)
mov    ax,#0x0000
mov    ds,ax
lds    si,[4*0x41]
mov    ax,#INITSEG
mov    es,ax
mov    di,#0x0004
mov    cx,#0x10
! 重复16次
rep
movsb

‍

显示获得的参数

现在已经将硬件参数(只包括光标位置、内存大小和硬盘参数,其他硬件参数取出的方法基本相同,此处略去)取出来放在了 0x90000 处,接下来的工作是将这些参数显示在屏幕上。这些参数都是一些无符号整数,所以需要做的主要工作是用汇编程序在屏幕上将这些整数显示出来。

以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 4 位二进制数和 1 位十六进制数存在一一对应关系),显示时只需将原二进制数每 4 位划成一组,按组求对应的 ASCII 码送显示器即可。ASCII 码与十六进制数字的对应关系为:0x30 ~ 0x39 对应数字 0 ~ 9,0x41 ~ 0x46 对应数字 a ~ f。从数字 9 到 a,其 ASCII 码间隔了 7h,这一点在转换时要特别注意。为使一个十六进制数能按高位到低位依次显示,实际编程中,需对 bx 中的数每次循环左移一组(4 位二进制),然后屏蔽掉当前高 12 位,对当前余下的 4 位(即 1 位十六进制数)求其 ASCII 码,要判断它是 0 ~ 9 还是 a ~ f,是前者则加 0x30 得对应的 ASCII 码,后者则要加 0x37 才行,最后送显示器输出。以上步骤重复 4 次,就可以完成 bx 中数以 4 位十六进制的形式显示出来。

下面是完成显示 16 进制数的汇编语言程序的关键代码,其中用到的 BIOS 中断为 INT 0x10,功能号 0x0E(显示一个字符),即 AH=0x0E,AL=要显示字符的 ASCII 码。

! 以 16 进制方式打印栈顶的16位数
print_hex:
! 4 个十六进制数字
    mov cx,#4
! 将(bp)所指的值放入 dx 中,如果 bp 是指向栈顶的话
    mov dx,(bp)
print_digit:
! 循环以使低 4 比特用上 !! 取 dx 的高 4 比特移到低 4 比特处。
    rol dx,#4
! ah = 请求的功能值,al = 半字节(4 个比特)掩码。
    mov ax,#0xe0f
! 取 dl 的低 4 比特值。
    and al,dl
! 给 al 数字加上十六进制 0x30
    add al,#0x30
    cmp al,#0x3a
! 是一个不大于十的数字
    jl  outp
! 是a~f,要多加 7
    add al,#0x07
outp:
    int 0x10
    loop    print_digit
    ret
! 这里用到了一个 loop 指令;
! 每次执行 loop 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则转移到 loop 指令后的标号处,实现循环;
! 如果为0顺序执行。
!
! 另外还有一个非常相似的指令:rep 指令,
! 每次执行 rep 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则继续执行 rep 指令后的串操作指令,直到 cx 为 0,实现重复。

! 打印回车换行
print_nl:
! CR
    mov ax,#0xe0d
    int 0x10
! LF
    mov al,#0xa
    int 0x10
    ret

‍

只要在适当的位置调用 print_bx 和 print_nl(注意,一定要设置好栈,才能进行函数调用)就能将获得硬件参数打印到屏幕上,完成此次实验的任务。但事情往往并不总是顺利的,前面的两个实验大多数实验者可能一次就编译调试通过了(这里要提醒大家:编写操作系统的代码一定要认真,因为要调试操作系统并不是一件很方便的事)。但在这个实验中会出现运行结果不对的情况(为什么呢?因为我们给的代码并不是 100% 好用的)。所以接下来要复习一下汇编,并阅读《Bochs 使用手册》,学学在 Bochs 中如何调试操作系统代码。

我想经过漫长而痛苦的调试后,大家一定能兴奋地得到下面的运行结果:

​

‍

图 4 用可以打印硬件参数的 setup.s 进行引导的结果Memory Size 是 0x3C00KB,算一算刚好是 15MB(扩展内存),加上 1MB 正好是 16MB,看看 Bochs 配置文件 bochs/bochsrc.bxrc:

!……
megs: 16
!……
ata0-master: type=disk, mode=flat, cylinders=410, heads=16, spt=38
!……

这些都和上面打出的参数吻合,表示此次实验是成功的。

实验楼的环境中参数可能跟上面给出的不一致。大家需要根据自己环境中 bochs/bochsrc.bxrc​ 文件中的内容才能确定具体的输出信息。

下面是提供的参考代码,大家可以根据这个来进行编写代码:

INITSEG  = 0x9000
entry _start
_start:
! Print "NOW we are in SETUP"
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#25
    mov bx,#0x0007
    mov bp,#msg2
    mov ax,cs
    mov es,ax
    mov ax,#0x1301
    int 0x10

    mov ax,cs
    mov es,ax
! init ss:sp
    mov ax,#INITSEG
    mov ss,ax
    mov sp,#0xFF00

! Get Params
    mov ax,#INITSEG
    mov ds,ax
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov [0],dx
    mov ah,#0x88
    int 0x15
    mov [2],ax
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x41]
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0004
    mov cx,#0x10
    rep
    movsb

! Be Ready to Print
    mov ax,cs
    mov es,ax
    mov ax,#INITSEG
    mov ds,ax

! Cursor Position
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#18
    mov bx,#0x0007
    mov bp,#msg_cursor
    mov ax,#0x1301
    int 0x10
    mov dx,[0]
    call    print_hex
! Memory Size
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#14
    mov bx,#0x0007
    mov bp,#msg_memory
    mov ax,#0x1301
    int 0x10
    mov dx,[2]
    call    print_hex
! Add KB
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#2
    mov bx,#0x0007
    mov bp,#msg_kb
    mov ax,#0x1301
    int 0x10
! Cyles
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#7
    mov bx,#0x0007
    mov bp,#msg_cyles
    mov ax,#0x1301
    int 0x10
    mov dx,[4]
    call    print_hex
! Heads
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#8
    mov bx,#0x0007
    mov bp,#msg_heads
    mov ax,#0x1301
    int 0x10
    mov dx,[6]
    call    print_hex
! Secotrs
    mov ah,#0x03
    xor bh,bh
    int 0x10
    mov cx,#10
    mov bx,#0x0007
    mov bp,#msg_sectors
    mov ax,#0x1301
    int 0x10
    mov dx,[12]
    call    print_hex

inf_loop:
    jmp inf_loop

print_hex:
    mov    cx,#4
print_digit:
    rol    dx,#4
    mov    ax,#0xe0f
    and    al,dl
    add    al,#0x30
    cmp    al,#0x3a
    jl     outp
    add    al,#0x07
outp:
    int    0x10
    loop   print_digit
    ret
print_nl:
    mov    ax,#0xe0d     ! CR
    int    0x10
    mov    al,#0xa     ! LF
    int    0x10
    ret

msg2:
    .byte 13,10
    .ascii "NOW we are in SETUP"
    .byte 13,10,13,10
msg_cursor:
    .byte 13,10
    .ascii "Cursor position:"
msg_memory:
    .byte 13,10
    .ascii "Memory Size:"
msg_cyles:
    .byte 13,10
    .ascii "Cyls:"
msg_heads:
    .byte 13,10
    .ascii "Heads:"
msg_sectors:
    .byte 13,10
    .ascii "Sectors:"
msg_kb:
    .ascii "KB"

.org 510
boot_flag:
    .word 0xAA55

‍

‍

‍

‍

‍

# 扩展

你可以显示不同的字符串,例如修改显示的字符串的颜色等等,或者可以实现载入system模块等等,完全可以自己有想做的功能,就做着玩~

可以参考hoverwinter/HIT-OSLab: S - 哈工大《操作系统》实验 (opens new window),这里就实现了system模块的载入,并且实验手册也写的很棒

‍

‍

# 附录

‍

# int 0x10显示服务

AH 功能 调用参数 返回参数
0x03 读光标位置和大小 BH=页号(0-based) CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based)
0x0e 显示字符(光标前移) AL=字符 BL=前景色 None
0x0f 获取当前显示模式 None AL=当前的显示模式,AH=屏幕宽度,以字符列,BH=当前页号(0-based)
0x1300 显示字符串光标留在起始位置 ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 None
0x1301 显示字符串光标跟随移动 ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 None
上次更新: 2022/10/29 11:17:58
搭建实验环境
Linux

← 搭建实验环境 Linux→

最近更新
01
操作系统接口
10-30
02
操作系统历史与学习任务
10-30
03
搭建实验环境
10-30
更多文章>
Theme by Vdoing | Copyright © -2022 粤ICP备2022067627号-1 粤公网安备 44011302003646号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式