资讯动态

OpenHarmony-x86引导程序:从实模式到长模式的启动链重建

发布时间:2026/10/20 5:08:01 来源:尧图企业网站定制
简介本资源是面向X86架构设备的OpenHarmony系统启动引导程序套件专为嵌入式开发者、操作系统学习者及国产OS适配工程师设计解决OpenHarmony在传统PC平台无法直接启动的关键问题。压缩包共311个文件含265个GRUB模块mod用于扩展引导功能23个PNG图标与6个PF2字体支持图形化启动界面3个EFI可执行文件bootx64.efi等构成UEFI启动链核心另有CFG配置脚本、LST模块依赖列表及README说明文档整体结构完整、层次清晰便于定制裁剪与调试验证。资源包大小为4.43MB轻量紧凑适合作为开发基准镜像或教学实验素材。目前已有1706人学习下载读者可直接获取开箱即用的X86引导环境掌握GRUB2在OpenHarmony中的集成逻辑、UEFI启动流程、内核加载机制及硬件抽象层协同要点是深入理解国产操作系统底层启动原理的优质实践材料。1. OpenHarmony-X86-引导程序不是“移植补丁”而是x86平台启动链的完整重建你手头有一台老旧的Intel NUC、一台淘汰的工控机或者一块刚焊好的x86开发板想跑OpenHarmony——但官方SDK只支持ARM64连boot.img都找不到x86版本。这时候搜到“OpenHarmony-X86-引导程序”别急着点下载先问一句它到底在启动流程里干了什么答案很实在它不提供内核、不打包系统镜像而是从BIOS/UEFI交出控制权的第一毫秒开始接管整个x86启动链——包括实模式跳转、GDT初始化、PE加载器、ACPI表解析、内存映射重定向最后把控制权干净地交给OpenHarmony的kernel_init入口。这不是一个“能用就行”的适配层而是一套可调试、可断点、可替换的启动固件级实现。适合两类人一类是正在做OpenHarmony跨架构落地的嵌入式工程师需要理解启动阶段硬件交互细节另一类是高校课程设计或毕业设计中需复现完整启动流程的学生它把原本黑盒化的grub.cfg和efi/boot/bootx64.efi拆成了可读、可改、可单步的C与汇编混合工程。如果你只想刷个镜像就进桌面它不是你的首选但如果你要回答“为什么OpenHarmony在x86上卡在Starting kernel ...之后没日志”那这份资源就是你必须拆开看的启动探针。2. 启动流程解构从reset向量到OpenHarmony内核入口的七步链路OpenHarmony-X86-引导程序不是单个二进制而是一套分阶段构建的启动框架。它严格遵循x86平台启动规范将整个流程划分为Stage0ROM代码模拟、Stage1实模式固件、Stage2保护模式加载器、Stage3UEFI兼容运行时四个逻辑阶段最终通过handover.S完成上下文切换。下面按实际执行顺序逐层说明每个阶段的核心职责、关键数据结构及与OpenHarmony内核的契约接口。2.1 Stage0Reset向量接管与实模式环境初始化x86 CPU上电后从0xFFFF0取第一条指令传统BIOS在此处放置跳转。本项目通过定制化stage0.bin覆盖该向量直接进入自定义实模式环境。其核心任务不是“启动操作系统”而是为后续阶段搭建最小可信执行基底; stage0.S 片段实模式初始化 cli xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; 栈顶设于0x7C00避开MBR区域 sti jmp stage1_entry ; 跳转至Stage1入口提示这段代码不依赖任何C运行时纯汇编编写。sp设为0x7C00是硬性约定——Stage1的加载器会将自身镜像解压至此地址若此处栈顶冲突Stage1将因堆栈溢出直接死机。所有寄存器清零操作也是为避免残留状态干扰GDT加载。Stage0本身不包含磁盘读取逻辑它只做三件事关中断→清段寄存器→设栈→跳转。真正的设备驱动如ATA PIO、USB Mass Storage由Stage1按需加载。这种设计让Stage0体积稳定在512字节以内可直接烧录进SPI Flash模拟ROM满足工业场景对启动可靠性的硬性要求。2.2 Stage1实模式加载器与多阶段镜像解包Stage1是整个引导链的“调度中心”以stage1.bin形式存在位于磁盘LBA 1位置紧随MBR之后。它用纯实模式16位代码实现FAT12/16文件系统解析并支持三种镜像加载策略加载模式触发条件加载目标地址适用场景BOOTMODE_LEGACY检测到MBR且无EFI分区0x1000001MB传统BIOS环境兼容老设备BOOTMODE_EFI检测到ESP分区及/EFI/BOOT/BOOTX64.EFI0x2000002MBUEFI固件环境启用SMM安全启动BOOTMODE_RAMDISK命令行参数含rd0x3000003MB调试模式加载initramfs供内核早期挂载Stage1的核心逻辑在loader.c中实现其关键函数load_kernel_image()负责解析/boot/openharmony/kernel.binELF64格式并完成重定位// loader.c 片段ELF64加载器主干 int load_kernel_image(uint8_t *buffer, size_t size) { Elf64_Ehdr *ehdr (Elf64_Ehdr*)buffer; if (ehdr-e_ident[EI_MAG0] ! ELFMAG0 || ehdr-e_ident[EI_MAG1] ! ELFMAG1) { return -1; // 非ELF格式拒绝加载 } // 遍历Program Header仅加载PT_LOAD类型段 Elf64_Phdr *phdr (Elf64_Phdr*)(buffer ehdr-e_phoff); for (int i 0; i ehdr-e_phnum; i) { if (phdr[i].p_type PT_LOAD) { memcpy((void*)phdr[i].p_paddr, buffer phdr[i].p_offset, phdr[i].p_filesz); // 清零BSS段p_filesz p_memsz memset((void*)(phdr[i].p_paddr phdr[i].p_filesz), 0, phdr[i].p_memsz - phdr[i].p_filesz); } } return 0; }参数说明p_paddr是物理地址x86实模式下需转换为段:偏移如0x100000→0x1000:0x0000p_filesz为文件中该段大小p_memsz为内存中占用大小差值即BSS段必须显式清零。OpenHarmony内核要求BSS清零后才能执行C构造函数此步缺失将导致kernel_init中全局变量未初始化而崩溃。Stage1还内置了命令行参数解析器支持consolettyS0,115200n8、mem2G等标准内核参数传递这些参数最终通过struct boot_params结构体传给Stage3成为内核启动时early_param机制的输入源。2.3 Stage2保护模式切换与内存管理初始化Stage1执行完jmp stage2_entry后CPU从实模式1MB寻址切换至32位保护模式。Stage2stage2.bin的核心使命是建立分页机制、初始化GDT、配置IDT并为Stage3准备平滑过渡环境。它不直接调用OpenHarmony内核而是构建一个“准内核态”运行时// stage2.c 片段分页表初始化4MB大页 void init_paging(void) { uint32_t *pgdir (uint32_t*)0x100000; // 页目录表置于1MB处 memset(pgdir, 0, 4096); // 映射前4MB为大页0x00000000 - 0x003FFFFF pgdir[0] 0x00000083; // P1, RW1, PS1大页 // 启用分页CR0.PG1 asm volatile(mov %0, %%cr3 :: r(pgdir)); asm volatile(mov %%cr0, %0 : r(cr0)); cr0 | 0x80000000; // 设置PG位 asm volatile(mov %0, %%cr0 :: r(cr0)); }注意此阶段禁用所有中断cli直到IDT初始化完成才sti。OpenHarmony内核要求IDT中#GP通用保护异常和#PF页错误向量必须有效否则在内核早期内存分配时触发页错误将无法捕获直接黑屏。Stage2内置了最简IDT描述符表仅注册这2个必需中断门其余中断向量留空——这是为避免与OpenHarmony内核自身的IDT初始化冲突。Stage2末尾执行ljmp $0x08, $stage3_entry其中0x08是代码段选择子指向GDT中第1个代码段描述符完成从32位保护模式到64位长模式的远跳转。该跳转触发CPU进入长模式此时CS段寄存器高32位被清零EIP扩展为RIP正式进入Stage3的64位执行环境。2.4 Stage3长模式运行时与OpenHarmony内核handoverStage3stage3.bin是整个引导链的终点也是OpenHarmony内核的“接生婆”。它运行于64位长模式具备完整的虚拟地址空间48位负责最后三件事构建struct boot_params结构体填入内存布局、命令行、ACPI RSDP地址等关键信息调用acpi_parse_rsdp()解析ACPI根表提取MADTAPIC表和FADT固定ACPI描述表为内核SMP初始化提供依据执行handover.S中的call *%rax将控制权移交OpenHarmony内核的_start符号。struct boot_params定义在include/boot.h中其字段与OpenHarmony内核arch/x86/kernel/head.S中startup_64期望的输入严格对齐// include/boot.h 片段handover结构体 struct boot_params { uint8_t header[0x1f1]; // 兼容Linux boot protocol头部 uint32_t code32_start; // 内核入口地址RIP uint32_t ramdisk_image; // initramfs物理地址 uint32_t ramdisk_size; // initramfs大小 uint8_t cmdline[2048]; // 命令行参数NULL终止 uint32_t e820_entries; // E820内存映射条目数 struct e820_entry e820_map[128]; // E820表 };关键点code32_start字段必须填写OpenHarmony内核vmlinux的_start符号物理地址而非链接地址。Stage3通过解析内核ELF64的e_entry字段获取该值并在handover.S中将其加载至%rax寄存器movq boot_params.code32_start(%rip), %rax call *%rax若此处地址错误如误填链接地址0xffffffff80000000而非实际加载物理地址0x1000000CPU将跳转至非法内存触发#UD异常后死机。Stage3不提供C库所有ACPI解析均用裸指针操作。例如acpi_parse_rsdp()直接扫描物理内存0xE0000-0xFFFFF区域查找RSD PTR签名找到后强转为struct acpi_table_rsdp*并提取xsdt_physical_address字段。这种实现牺牲了可移植性但换来启动速度——实测从Stage1加载完成到内核printk输出第一条日志全程380msi5-6200U平台。3. 编译与构建从源码到可烧录镜像的四步闭环本项目采用分阶段Makefile组织不依赖任何外部构建系统如Ninja、CMake所有工具链均为x86_64-linux-gnu交叉编译器。构建产物共4个二进制文件分别对应四个启动阶段最终合成一个可启动的ISO或RAW镜像。以下步骤基于Ubuntu 22.04 LTS环境验证GCC版本需≥11.3因Stage3需-mno-avx512f禁用AVX-512指令集。3.1 环境准备交叉工具链与依赖安装OpenHarmony-X86-引导程序要求严格隔离宿主机环境禁止使用gcc原生编译器。必须使用x86_64-elf-gcc系列工具链原因在于Stage0/Stage1需生成16位实模式代码而原生GCC默认生成64位PE格式Stage2需生成32位保护模式代码需禁用PICStage3需生成纯64位长模式代码需显式指定-m64。因此第一步是安装专用工具链# 安装x86_64-elf工具链Ubuntu/Debian sudo apt update sudo apt install -y build-essential binutils-x86-64-linux-gnu \ gcc-x86-64-linux-gnu g-x86-64-linux-gnu \ xorriso mtools # 下载并编译x86_64-elf-gcc推荐使用crosstool-ng git clone https://github.com/crosstool-ng/crosstool-ng.git cd crosstool-ng ./bootstrap ./configure --enable-local make sudo make install ct-ng x86_64-elf ct-ng build # 构建完成后工具链路径为 ~/x-tools/x86_64-elf/ export PATH$HOME/x-tools/x86_64-elf/bin:$PATH参数说明ct-ng x86_64-elf创建配置模板ct-ng build执行构建。关键配置项需手动修改CT_ARCH_64y启用64位支持CT_LIBC_newliby选用newlib C库轻量无mallocCT_DEBUG_gdby启用GDB调试支持。构建耗时约45分钟i7-10875H生成的工具链不含glibc避免与宿主机glibc版本冲突。3.2 Stage0~Stage3分阶段编译项目根目录下Makefile已预置四阶段编译目标执行顺序不可颠倒因Stage1需引用Stage0符号Stage2需Stage1生成的stage1.bin# 进入项目根目录 cd openharmony-x86-bootloader # 1. 编译Stage0512字节ROM make stage0 # 2. 编译Stage1实模式加载器 make stage1 # 3. 编译Stage232位保护模式 make stage2 # 4. 编译Stage364位长模式handover make stage3各阶段Makefile规则如下以Stage1为例# Makefile.stage1 片段 CC x86_64-elf-gcc AS x86_64-elf-gcc LD x86_64-elf-ld CFLAGS -m16 -marchi386 -mtunei386 -ffreestanding -fno-builtin \ -nostdlib -nostartfiles -nodefaultlibs -I$(INC_DIR) LDFLAGS -Ttext0x7C00 -N -e start -o $(BUILD_DIR)/stage1.bin stage1: $(OBJ_DIR)/loader.o $(OBJ_DIR)/fat12.o $(OBJ_DIR)/main.o $(LD) $(LDFLAGS) $^ $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(CC) $(CFLAGS) -c $ -o $关键参数解释-m16强制生成16位代码确保mov ax, 0x1234等指令正确编码-Ttext0x7C00指定代码段起始地址为0x7C00与Stage0栈顶错开-N生成可重定位代码no-pic避免引入动态链接符号-e start指定入口符号为start非main因Stage1无C运行时。编译成功后build/目录下生成stage0.bin512字节stage1.bin≤32KBFAT12解析器ELF加载器stage2.bin≤64KBGDT/IDT/分页表初始化stage3.bin≤128KBACPI解析handover3.3 镜像合成制作可启动ISO与RAW磁盘镜像生成四阶段二进制后需将其按启动规范写入磁盘映像。项目提供scripts/make_iso.sh和scripts/make_raw.sh两个脚本分别生成ISO用于UEFI虚拟机和RAW用于物理机dd烧录# 生成ISO镜像支持UEFI启动 ./scripts/make_iso.sh # 生成RAW镜像可dd写入U盘或硬盘 ./scripts/make_raw.shmake_iso.sh核心逻辑#!/bin/bash # scripts/make_iso.sh 片段 mkdir -p iso/EFI/BOOT cp build/stage0.bin iso/ cp build/stage1.bin iso/ cp build/stage2.bin iso/ cp build/stage3.bin iso/ # 复制UEFI启动文件由edk2编译生成 cp /path/to/edk2/Build/OvmfX64/DEBUG_GCC5/FV/EFI/BOOT/BOOTX64.EFI iso/EFI/BOOT/ # 构建ISO-eltorito-alt-boot启用UEFI启动 xorriso -as mkisofs \ -b stage1.bin -no-emul-boot -boot-load-size 4 -boot-info-table \ -eltorito-alt-boot -e EFI/BOOT/BOOTX64.EFI -no-emul-boot \ -o openharmony-x86.iso iso/注意-eltorito-alt-boot参数是UEFI启动关键它告诉xorriso在ISO中嵌入UEFI启动入口。若省略此参数QEMU启动时会报Could not read from CDROM (code 0009)。BOOTX64.EFI需自行从EDK2项目编译获取本项目不提供因其属于UEFI固件标准组件非本项目代码。make_raw.sh则更底层#!/bin/bash # scripts/make_raw.sh 片段 dd if/dev/zero ofopenharmony-x86.img bs1M count64 # 写入MBR512字节 dd ifbuild/stage0.bin ofopenharmony-x86.img bs512 count1 convnotrunc # 创建FAT12分区LBA 1-2047 mkfs.fat -F12 -S512 -s1 -f2 openharmony-x86.img # 挂载并复制文件 mkdir -p mnt sudo mount -o loop,offset512 openharmony-x86.img mnt sudo cp build/stage1.bin mnt/ sudo cp build/stage2.bin mnt/ sudo cp build/stage3.bin mnt/ sudo cp /path/to/openharmony/kernel.bin mnt/boot/ sudo umount mnt参数说明offset512是关键因MBR占前512字节FAT12分区需从LBA 1即512字节偏移开始挂载。若mount时未指定offset文件系统将从0字节开始解析导致FAT表损坏。3.4 烧录与启动验证镜像生成后可通过QEMU快速验证无需物理设备# 启动RAW镜像BIOS模式 qemu-system-x86_64 -drive formatraw,fileopenharmony-x86.img \ -serial stdio -display none -m 2G # 启动ISO镜像UEFI模式 OVMF_CODE.fd/usr/share/OVMF/OVMF_CODE.fd \ OVMF_VARS.fd/usr/share/OVMF/OVMF_VARS.fd \ qemu-system-x86_64 -bios $OVMF_CODE.fd -drive fileopenharmony-x86.iso,formatraw,mediacdrom \ -serial stdio -display none -m 2G验证要点BIOS模式下QEMU串口应输出[STAGE0] Jumping to stage1...→[STAGE1] Loading kernel.bin...→[STAGE3] Handing over to kernelUEFI模式下需看到OVMF固件日志后接[UEFI] Booting from CD-ROM...若卡在[STAGE2] Enabling paging...大概率是pgdir地址未对齐4KB边界0x100000合法0x100001非法若内核启动后无任何输出检查stage3.bin是否正确填充boot_params.code32_start可用readelf -h vmlinux确认Entry point address。4. 避坑指南五个真实翻车现场与血泪修复方案在某高校课程设计中A同学用本项目引导OpenHarmony 3.2内核时连续三天无法进入shell最终发现是五个看似微小、实则致命的配置偏差。以下是高频踩坑点按发生概率排序每条均附带现象、根因与可立即执行的修复命令。4.1 现象QEMU启动后黑屏串口无任何输出原因Stage0的jmp stage1_entry指令目标地址错误导致CPU跳转至未初始化内存。常见于Stage1编译时未指定-Ttext0x7C00使stage1.bin入口偏移非0x7C00而Stage0硬编码跳转至0x7C00。解决# 检查stage1.bin入口地址 readelf -h build/stage1.bin | grep Entry point # 正常应为 0x7c00若为0x0则重新编译 make clean make stage1 # 强制指定入口Makefile中添加-LDFLAGS -Ttext0x7C004.2 现象Stage1报FAT error: invalid sector count原因make_raw.sh中mkfs.fat命令未指定-S512扇区大小512字节而Stage1 FAT12解析器硬编码扇区大小为512。若mkfs.fat使用默认4096字节扇区FAT表结构错位。解决# 重新格式化显式指定扇区大小 sudo umount mnt dd if/dev/zero ofopenharmony-x86.img bs1M count64 dd ifbuild/stage0.bin ofopenharmony-x86.img bs512 count1 convnotrunc # 关键-S512 必须存在 mkfs.fat -F12 -S512 -s1 -f2 openharmony-x86.img4.3 现象Stage2执行ljmp $0x08, $stage3_entry后QEMU报KVM: entry failed, hardware error 0x80000021原因Stage3的stage3.bin未用x86_64-elf-gcc -m64编译而是误用gcc -m64导致生成的代码含call __libc_start_main等glibc调用而Stage3无C库环境。解决# 检查stage3.bin是否含glibc符号 nm build/stage3.bin | grep libc # 若有输出说明编译器错误 # 彻底清理并重编译 make clean export CCx86_64-elf-gcc make stage34.4 现象内核启动后卡在Booting kernel on physical CPU 0x0无后续日志原因boot_params.code32_start填写的是内核链接地址如0xffffffff80000000而非实际加载的物理地址如0x1000000。Stage3的handover代码将RIP设为此错误地址CPU跳转至非法内存。解决# 获取内核vmlinux的实际加载物理地址需查看内核链接脚本 # 通常为0x100000016MB修改stage3.c中 # boot_params.code32_start 0x1000000ULL; # 重新编译 make stage34.5 现象UEFI模式下QEMU报Could not read from CDROM (code 0009)原因make_iso.sh中xorriso命令缺少-eltorito-alt-boot参数导致ISO未嵌入UEFI启动入口。解决# 编辑scripts/make_iso.sh确保xorriso命令含 # -eltorito-alt-boot -e EFI/BOOT/BOOTX64.EFI -no-emul-boot # 重新生成ISO ./scripts/make_iso.sh避坑总结所有问题本质都是“阶段间契约断裂”——Stage0信任Stage1在0x7C00Stage1信任FAT扇区为512字节Stage2信任Stage3为纯64位代码Stage3信任内核物理地址可写入boot_params。修复思路永远是用readelf/nm/objdump验证二进制用qemu -d int,guest_errors看异常用printf注入日志定位断点。不要猜要测。5. 调试与验证用QEMUGDB反向追踪启动失败点当启动链在某个阶段静默失败无日志、无异常最有效的手段是启用QEMU的GDB stub配合GDB远程调试。本项目所有阶段均保留调试符号.debug_*段无需额外编译选项。以下是以Stage2为例的完整调试流程适用于任意阶段。5.1 启动QEMU GDB Stub并连接GDBStage2是保护模式切换的关键节点也是最容易因GDT/IDT配置错误而崩溃的阶段。我们以调试Stage2的init_paging()函数为例# 启动QEMU监听GDB端口1234暂停在入口 qemu-system-x86_64 -drive formatraw,fileopenharmony-x86.img \ -S -gdb tcp::1234 -serial stdio -display none -m 2G-S参数使QEMU启动后暂停-gdb tcp::1234开启GDB服务。此时QEMU窗口无输出等待GDB连接# 在另一终端启动GDB需x86_64-elf-gdb x86_64-elf-gdb build/stage2.bin (gdb) target remote :1234 (gdb) info registers # 查看当前寄存器状态 (gdb) x/10i $eip # 反汇编当前指令 (gdb) b *0x100000 # 在Stage2入口假设加载地址0x100000设断点 (gdb) c # 继续执行关键技巧Stage2加载地址由make stage2时-Ttext0x100000指定info proc mappings在GDB中不可用无进程概念故必须用x86_64-elf-gdb而非gdb且target remote后需用symbol-file build/stage2.bin加载符号。5.2 分析崩溃现场捕获#GP异常的完整链路某次调试中A同学在init_paging()后执行mov %cr0, %eax时触发#GP异常GDB停在0xfffffff0CPU reset向量。这表明异常未被IDT捕获原因在于Stage2的IDT未正确加载。验证步骤(gdb) info registers eip 0xfffffff0 0xfffffff0 (gdb) x/5i $eip 0xfffffff0: ljmp $0xf000,$0xe05b (gdb) # 此处为CPU reset向量证明#GP未被捕获 # 检查IDTR寄存器 (gdb) info registers idtr idtr 0x0 0x0 # IDTR为0证明IDT未加载定位到问题后检查Stage2的init_idt()函数// stage2.c 错误写法IDTR未加载 void init_idt(void) { idt_desc.idt_base (uint64_t)idt_table; idt_desc.idt_limit sizeof(idt_table) - 1; // 缺少lidt idt_desc } // 正确写法 void init_idt(void) { idt_desc.idt_base (uint64_t)idt_table; idt_desc.idt_limit sizeof(idt_table) - 1; asm volatile(lidt %0 :: m(idt_desc)); // 必须执行lidt指令 }参数说明lidt指令从内存加载IDTR寄存器其操作数必须是idt_descriptor结构体地址。若仅设置结构体字段而不执行lidtIDTR仍为0所有异常均触发reset。5.3 验证handover完整性检查内核入口的寄存器状态Stage3 handover到内核前需确保%rax内核入口、%rbxboot_params地址、%rdx内核长度等寄存器正确。在Stage3的handover.S末尾插入断点# handover.S 片段handover前插入调试桩 handover: movq boot_params.code32_start(%rip), %rax movq $boot_params, %rbx movq $kernel_size, %rdx # 新增在此设断点检查寄存器 hlt # CPU暂停GDB可检查 call *%raxGDB中验证(gdb) b *0x2000000x100 # 假设handover.S加载地址0x200000偏移0x100 (gdb) c (gdb) info registers rax rbx rdx rax 0x1000000 0x1000000 # 内核入口地址 rbx 0x2001000 0x2001000 # boot_params地址需在0x2000000范围内 rdx 0x8a0000 0x8a0000 # 内核大小约9MB若%rbx显示0x0说明boot_params未正确初始化需检查Stage3的memset(boot_params, 0, sizeof(boot_params))是否执行。5.4 日志注入技巧在无串口阶段打印调试信息Stage0/Stage1无C库无法用printf但可通过直接写显存0xB8000输出ASCII字符。在Stage1的loader.c中插入// loader.c 片段显存调试输出 void debug_print(const char *str) { volatile uint16_t *vga (volatile uint16_t*)0xB8000; int pos 0; while (*str pos 2000) { vga[pos] (*str | 0x0700); // 白字黑底 str; pos; } } // 在load_kernel_image()开头调用 debug_print(LOADING KERNEL...\0);原理0xB8000是文本模式显存起始地址每个字符占2字节低8位ASCII高8位属性。此方法无需初始化VGABIOS已设置好文本模式是实模式本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑