恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从CPU到App:一套教学框架让你亲手写透计算机系统

  • 首页
  • 资讯中心
  • /
  • 从CPU到App:一套教学框架让你亲手写透计算机系统

相关资讯

Spring 4.0.3整合Redis:spring-data-redis与Jedis配置实战 2026/10/14 17:39:16
少儿信息学复赛试题拆解:从模拟到动态规划的竞赛避坑指南 2026/10/14 17:34:15
HUAWEI HiCar 认证测试常见失败根因逐条拆解 2026/10/14 17:34:15

最新资讯

Python电商数据分析与随机森林销量预测全链路实战
Flutter AAR接入Android实战:存量App增量集成指南
Principia研究地图(Study Map)完全指南:如何从一条候选关系追踪到支持它的全部证据记录
STM8S003 GPIO模拟串口:IAR实现软UART发送与接收
SI4463 STM32源码包实战:SPI驱动、初始化与收发避坑指南
STM32智能门禁用uC/OS-III替代裸机开发的实战指南

今日推荐

IBM Verify Identity Access 修复 22 个漏洞:两个 9.8 严重 RCE 无需认证
小区物业管理系统
小型超市仓库管理系统

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

从CPU到App:一套教学框架让你亲手写透计算机系统

发布时间:2026/10/14 17:39:16
从CPU到App:一套教学框架让你亲手写透计算机系统 简介南京大学计算机系统课程ICS2017的编程作业项目是一套面向计算机专业学生的完整教学实验框架涵盖NEMU模拟器、NexusAM抽象层、NanosLite操作系统与NavyApps应用层四个递进模块帮助学习者在模拟环境中理解从微处理器到应用开发的系统全貌。资源包共877个文件压缩后18.6MB以C语言源文件538个.c、183个.h与Makefile构建脚本为主辅以少量C、Markdown文档及Python辅助脚本文件结构清晰、便于按模块研读。目前已有97人下载学习。NEMU模拟器可让学生脱离硬件亲自实现指令执行与寄存器逻辑NexusAM抽象层衔接硬件与软件接口NanosLite操作系统涵盖进程、内存、文件系统与驱动等核心机制NavyApps则提供可运行示例便于实操验证。附带的说明文档与Markdown文件对实验背景、使用流程和项目入口均有注释适合正在学习ICS课程或希望补强系统能力的中高年级本科生使用是一份可反复对照源码与文档进行深度实践的教学资料。1. 一套教学框架把 CPU 到 App 的每一层都串起来这可能是学计算机系统最好的方式很多开发者学过组成原理、操作系统、编译器但从一条指令到用户点开一个应用之间是怎么衔接的脑中往往是断的。这套被称作完整计算机系统教学实验框架的课程作业把 NEMU 模拟器、NexusAM 抽象层、NanosLite 操作系统、NavyApps 应用程序四层代码接成一条可运行的链路。它不靠 PPT而是让你亲手把一台机器从无到有点亮。如果你正在准备某高校计算机系统课程或者想给自己的项目搭一套能从零看到运行现场的实验室这套框架的价值在于它每一个层都不是摆设而是会在后面的实验里被你亲手改掉。我最早是从一个模拟器开始最后一路改到了操作系统那种所有代码都是自己的的感觉和只看书完全不同。这篇笔记会把四层结构、最小跑通命令和最常见的几个坑一次说清。2. 拆开四层模拟器、抽象层、操作系统和应用各自在解决什么2.1 NEMU 模拟器为什么不用通用虚拟机而是从零写一个先回答一个最常被问的问题有现成的通用虚拟机为什么还要自己写 NEMU因为通用虚拟机的执行引擎是黑匣子它把取指、译码、执行包在了一层又一层中间码转换里你无法在两节课内看穿它。而 NEMU 的设计目标就是教学可读性用几行 C 写出一个 while 循环每一轮从内存读指令、查表、执行、更新 PC。你不需要理解 KVM、TCG、二进制翻译只需要盯住一个cpu.pc指针。常见做法的核心循环大概长这样// nemu/src/cpu/exec.c 中的主循环教学简化版 while (cpu.pc ! -1) { // 从物理内存读一个 32 位指令字节序为小端 uint32_t instr vaddr_read(cpu.pc, 4); // 译码并执行exec_real_instr 会查 opcode_table 分发 exec_real_instr(instr); // 每执行完一条指令检查是否有外部事件时钟、按键等 cpu_update(); }逻辑说明cpu.pc是程序计数器初始值由硬件复位逻辑决定。vaddr_read不是简单内存读它要经过 NexusAM 的地址翻译这也解释了为什么第一层模拟器和第三层操作系统是绑定的。exec_real_instr内部维护一个opcode_table数组按 opcode 的高 6 位、中间 3 位、低 3 位逐级查找找到最后一级的处理函数后调用处理函数内部会更新寄存器并推进cpu.pc。这里的-1是停止条件NanosLite 调用halt后会让 PC 变成一个特殊值循环才会退出。参数说明vaddr_read的第一个参数是虚拟地址第二个参数是读取长度字节数支持 1、2、4返回的uint32_t在小端架构上就是指令编码。调试时NEMU 一般都有 monitor 命令比如输入x 0x100000查看内存、s单步、info r看寄存器。我建议你在跑通之前先手动执行info r确认eip的初始值和复位地址一致这是后面所有问题的基准也是你判断是不是我的代码改了 PC的依据。为什么用while而不是for因为 NEMU 要响应外部中断——时钟中断到来时cpu_update()会触发CTE自陷与上下文处理机制把当前上下文压栈再跳转到操作系统的中断入口这个流程需要每一轮指令后都有机会检查事件。如果写成for(;;) { exec; }且检查事件放在循环体外中断就永远等不到。这个点经常被忽略但它解释了 NEMU 这种边执行边看结构的必然性。2.2 NexusAM不是库是一份硬件访问合同NexusAM 的定位可以从名字猜出来抽象机器Abstract Machine。它把底层 NEMU 提供的物理资源全部收敛成一组AM_*API让 NanosLite 不用关心自己到底跑在哪个架构上。常见做法是// nexus-am/am/include/am.h 中暴露给操作系统的接口 typedef struct { uint32_t addr; int len; } AM_MemArg; // 向指定物理地址写 len 字节 void AM_WRITE(uint32_t addr, void *buf, int len); // 从指定物理地址读 len 字节 void AM_READ(uint32_t addr, void *buf, int len);这里写的是接口没有写实现。实现按架构分放例如nexus-am/am/src/x86/nemu/下是 x86 版 NEMU 的实现riscv 架构也有对应目录。参数addr是物理地址不是虚拟地址NanosLite 传给它的地址来自页表映射后的结果。AM_READ会把len拆成多个 4 字节读调用 NEMU 的paddr_read。逻辑上AM_WRITE是 NanosLite 所有输出串口、显示、磁盘的共同入口所以应用 printf 调 write 系统调用write 最终撞到 AM_WRITE 而不是直接操作端口这是教学框架刻意做的分层。除了内存读写NexusAM 还约定时钟、键盘、串口、自陷、内存管理六个主要模块。每类模块都是一组AM_TIMER_*、AM_INPUT_*、AM_UART_*的只读/写函数。设计这些接口时有个原则一次操作只做一件事不提供批处理接口。因为教学 OS 不需要性能简单直接才能暴露问题。实际使用中我最常踩坑的地方是AM_READ/AM_WRITE的len参数和 NEMU 的paddr_read长度不匹配。NEMU 的物理内存接口往往要求len是 4 的整数倍但上层传的len是字符个数比如write(fd, buf, 5)中的 5如果不做拆分和拼装就会读到越界数据。我的习惯是在AM_WRITE实现里先打印addr % 4和len看到非对齐情况时主动处理而不是把问题留给 OS 层。2.3 NanosLite一个只有必要功能的操作系统NanosLite 这个名称带个 Lite但它并不是玩具——它是精心裁剪过的教学 OS。它要证明的命题是操作系统能加载应用应用能调系统调用系统调用能落到硬件。所以它有中断入口、系统调用分发、一个进程、一个简单的堆管理但没有文件系统、没有守护进程。第一次在 NEMU 里把 NanosLite 跑起来你会看到它先完成一堆初始化然后跳进_start加载应用镜像最终进入用户程序。这个顺序本身就是一张系统启动教科书。下面的代码是操作系统里最容易被看漏的部分——系统调用分发// nanos-lite/src/syscall.c 中的系统调用主入口 uintptr_t do_syscall(uint32_t a0, uint32_t a1, uint32_t a2) { switch (a0) { case SYS_exit: halt(a1); break; case SYS_write: write(a1, a2, a3); break; case SYS_read: read(a1, a2, a3); break; default: panic(Unhandled syscall: %d, a0); } return 0; }这里参数名a0/a1/a2对应应用程序调用syscall时传的参数顺序和架构体系结构有关系。x86 用户程序通过自陷指令触发调用后CPU 会把用户栈切换到内核栈同时把用户态的寄存器现场保存到上下文结构体里do_syscall再从结构体里取eax作为系统调用号。常见的翻车点应用侧编译时用错了库printf根本没有走到这个do_syscall而是走到处理器直接 panic。所以遇到输出为空先用info r看eax的值再对照SYS_xxx宏定义确认两边编号一致。NanosLite 的启动代码也值得读一遍它往往靠一段手写汇编完成从模拟器跳转到 OS 入口# nanos-lite/src/start.S 的简化结构不是实际源码 .globl _start _start: # 初始化栈指针栈顶通常在内存高地址 movl $stack_top, %esp # 清空未初始化数据的 BSS 段 call init_bss # 调用 C 的 main call main逻辑说明stack_top来自链接脚本它的值决定了操作系统能用多大栈。init_bss必须在你第一次 malloc 前完成否则未被清零的数据会让堆管理器的内存链表直接挂掉。这些引导汇编对新手来说是玄学但它是整条链路最容易出错的地方栈没设对第一条 C 函数调用就可能把返回地址写到非法内存。我建议你亲手把stack_top换成一个小值观察崩溃位置是不是从main附近变成在init_bss附近这比任何教程都能让你理解栈的作用。2.4 NavyApps应用层把标准库变薄最后一层是应用程序包。NavyApps 里的hello程序也许是整个框架里最简单但最关键的代码。它证明的不是会写 printf而是printf 在操作系统上到底依赖谁。教学框架提供的 libc 极简printf通常直接对接write系统调用不做缓冲也不做浮点解析因为没必要。// navy-apps/libc/src/printf.c 的逻辑示意 void printf(const char *fmt, ...) { char buf[128]; // 这里做一个简单的格式化子集 vsnprintf(buf, sizeof(buf), fmt, ap); // 直接调用 write 系统调用不是标准库的 fwrite write(1, buf, len); }参数说明第一个参数1是文件描述符对应串口输出write在 NanosLite 中最终会往 NexusAM 的串口写字符再由 NEMU 把字符打印到终端。别小看这个简化 printf它砍掉了标准库的缓冲和锁意味着你的应用不能假设多次 printf 是一起输出的。如果你在自己的应用里想用浮点%f大概率要在 libc 里补实现——这是许多同学做到中期实验时才发现必须先扩展框架的地方。NavyApps 的构建依赖分两个方向一是依赖 NanosLite 提供的加载格式二是依赖 NexusAM 提供的klib基础函数库比如memcpy、strlen的裸机版。所以编译顺序必须是 NEMU → NexusAM → NanosLite → NavyApps反过来编译几乎必错。如果只想跑 hello其实可以先把其他应用都从 Makefile 的APPS变量里注释掉减少编译时间跑通后再逐个加回来。3. 从解压到跑通 hello三个关键步骤和最小命令3.1 环境准备一个干净的 Linux 容器最省心我用过真实物理机也在容器里跑过结论是用容器最省心前提是容器里装了 32 位编译库且内核支持 32 位兼容。物理机往往会被显卡驱动、旧库版本干扰而容器可以随时推倒重来。第一次做这个实验时我在物理机上卡了整整一个下午换容器后十分钟跑通所以强烈建议你也按宁可重装、不要硬修的思路来准备环境。# 在基于 apt 的 Linux 发行版上安装依赖 sudo apt update sudo apt install -y build-essential git libx11-dev \ libsdl2-dev libreadline-dev bison flex # 如果是 64 位系统还需要 32 位编译支持 sudo apt install -y gcc-multilib g-multilib参数说明gcc-multilib是整个实验能否编译 32 位代码的关键libsdl2-dev只在运行带图形窗口的应用时需要如果你只做 hello 实验可以不管但装上能避免后期开发时重新搭建环境。bison和flex只在重新生成 NEMU 的译码文件时需要默认提供好的文件不需要但我建议还是装上因为课程实验中往往会有修改指令译码规则的环节临时装容易打断思路。我一般还会先跑一句make -v和gcc -m32 -v判断两件事一是 make 版本是否太老导致并行构建的行为不稳定二是gcc -m32能否生成可执行文件。如果gcc -m32直接报错就算 apt 安装成功也说明多库有问题需要重启终端再试。这个前置检查只需一分钟能帮你省掉后面所有明明装了一堆却还是编译不过的泪。3.2 按依赖顺序编译先模拟器、再抽象层、然后操作系统这套框架的 Makefile 设计得很像真实工程每一层有独立 Makefile上一层的库被下一层引用。编译顺序错了会看到找不到头文件或未定义的引用。我常用的命令组是cd /path/to/root/nemu make -j1 # 编译模拟器首次建议串行 cd /path/to/root/nexus-am make -j1 # 构建 am 和 klib cd /path/to/root/nanos-lite make -j1 # 编译操作系统 cd /path/to/root/navy-apps make -j1 # 编译应用集合逻辑说明make -j1是串行能避免并行构建时底层头文件还没生成、上层就开编的随机失败。第一次编译用串行确认通过后再考虑-j2。每层编译完成以后建议留意输出末尾是不是有OK或者对应.a文件生成如果某层报错立即在该目录下重新执行一次不要跨层继续因为下一层的 Makefile 会依赖上一层的库文件缺失时会出现非常隐蔽的链接错误。我在实际跑的时候发现nexus-am层的 make 有两个常见位置库文件生成在build/子目录但上层引用时用的是相对路径。如果你的目录是从压缩包直接解压的注意不要更改Makefile中的ARCH和ISA变量除非你清楚自己在干什么。默认配置通常匹配x86 NEMU我第一次就是随手把ISA改成riscv32想试试结果整个系统在启动后第一行汇编就翻车。这说明先按默认跑通再去动配置这是一个值得遵守的规则。3.3 最小启动命令与到底有没有跑起来的判断方法编译完成后运行环节不需要额外命令一般在nanos-lite目录下直接执行cd /path/to/root/nanos-lite make runmake run通常内部会调 NEMU并把编译好的nanos-lite作为镜像传入。如果终端出现 Hello World 或者课程定义的成功标志那说明整条链路通了。但很多情况下你会看到 NEMU 启动了却没有任何输出。这时最先要确认的不是 NEMU 的代码而是make run背后的 Makefile 规则。打开nanos-lite/Makefile搜索run目标看它是否显式传了--image或IMAGE环境变量。有些版本要求make IMAGEpath/to/hello run不传的话 NEMU 默认执行内置的dummy自然不会打印 hello。接下来检查 NEMU 的串口输出是否打开。教学版 NEMU 有时支持-s参数关闭串口用于性能测试如果 Makefile 里的NEMUFLAGS带了-s你看到的静态输出就是空。问题是-s的副作用是所有printf都不可见所以你改了半天应用都看不到效果。解决是把-s删掉再make run。最后如果上述都对用 NEMU 的info r看eip停在哪配合指令 trace 文件分析。具体 trace 开关在 NEMU 的 monitor 段nemu -l trace.log。还有一个被我忽略过的细节NanosLite 启动前NEMU 需要把 hello 的二进制放在指定地址。如果 hello 的链接脚本入口地址和 NanosLite 的加载地址不一致程序会在第一条跳转指令处跑飞。判断方法看 trace 文件最后几条是不是在某个高地址循环循环体只有一条jmp。如果是那就不是代码逻辑问题是加载地址错位。修正方法是调整 navy-apps 里hello的链接脚本让它匹配nanos-lite的loader中写死的地址。这一条请务必记下来因为它就是你在下一章会遇到的典型翻车现场之一。4. 运行期避坑记录四个高频问题与排查顺序下面这四条是我在跑通这套框架时真实遇到的坑也问了几个同样在做的同学最后确认不是个例。每一条都会按现象、原因、解决三步写你可以直接照着排查。前两条出问题的概率最高后面两条更容易让你怀疑人生。4.1 编译 NEMU 时报 elf.h: No such file or directory现象在 64 位 Linux 上第一次make就报错头文件找不到而且错误信息里经常混着/usr/include/x86_64-linux-gnu/这样的路径。有时候还会连带报 bits/libc-header-start.h missing。原因NEMU 默认用-m32编译需要 32 位 glibc 的备份头文件而系统默认只装了 64 位版本。这不是缺内核头文件是缺gcc-multilib组件也就是 32 位编译库的整体支持。装过普通gcc没带上 multilib所以gcc -m32找不到对应的头文件路径。解决sudo apt install -y gcc-multilib g-multilib安装后不要直接继续make要先把之前的编译残留清掉cd /path/to/root/nemu make clean make -j1如果make clean后还是报同样的头文件错误多数情况是终端会话没有重新加载环境变量。我遇到过在同一个终端里装完库还报错新开一个终端就好了。注意gcc-multilib和g-multilib都要装只装前者时 C 文件的编译还会挂这个坑的变体是换了一台机器后无法复现因为另一台机器预装过多库。4.2 启动 NEMU 后黑屏或者输出一串乱码现象make run后 NEMU 窗口一片黑终端里一堆?或无法识别的字符有时还有 Invalid memory access 并直接退出。原因NEMU 的ISA配置和 NanosLite 的ISA配置不一致。比如 NEMU 用x86NanosLite 却用riscv32编译两者从复位向量开始就完全不同。你看到的现象往往是 NEMU 自己跑了一段复位代码然后跳到nanos-lite镜像的入口但入口处的字节对 x86 来说不是合法指令。栈顶、栈方向、寄存器位宽也不一致自然就是乱码。解决在nemu/Makefile和nanos-lite/Makefile里都查ISA变量统一改为一致的值再make clean make。我的经验是不要相信压缩包里的默认配置一定是配好的不同机器的环境变量可能导致ISA被覆盖。统一用命令行传参会更稳make ISAx86 -j1同时检查ARCH是否匹配ARCH通常决定编译目标的三元组比如x86_64-linux-gnu对应 x86 的 32 位目标还是 64 位目标与ISA不一致时也会有奇怪现象。如果ARCH写的是x86_64而你用ISAx86那编译出来的 NanosLite 还是 64 位指令NEMU 按 32 位译码一样会翻车。所以我的习惯是把ISA和ARCH两个变量都清空后重新指定避免继承环境里的旧值。4.3 应用一运行就段错误GDB 显示停在unexpected return from do_syscall现象hello 第一次运行不是输出内容而是在用户态某条ret指令处段错误。有时 NEMU 会直接提示 unexpected return from do_syscall意思是系统调用返回后没有回到正常执行流。原因系统调用do_syscall执行完halt或write后没能正确回到用户态。常见原因是自陷的上下文切换CTE没有把上一次保存的上下文恢复或者栈指针在进入内核态后没有被切换到一个独立的内核栈。NanosLite 如果只是简单call do_syscall没有特殊处理ret会返回到用户栈而用户栈的内容已经因为之前的中断压栈被破坏了。解决检查 CTE 的上下文结构是否在内核栈上分配并且中断处理函数是否在返回前把保存的寄存器值写回用户寄存器。具体做法是用 NEMU 的info r查看用户态栈指针和系统调用前的esp对比。如果发现esp变了多半是内核栈复用问题。可以尝试在start.S里把内核栈定义得更大一点或者检查CTE的保存恢复代码是否漏存了某个寄存器。这个问题是最耗时的因为它不是某个函数写错而是上下文切换的完整路径哪里断了。我的排查习惯是先确认halt系统调用能终止能终止说明自陷路径通了一半再单独测write。如果halt也返回说明问题不在系统调用的执行逻辑而在返回路径上重点看__am_irq_handle的尾处理。4.4 并行编译随机失败-j8时 virtual memory exhausted现象make -j8编译到一半突然报 virtual memory exhausted或者某次编译通过、某次报错且报错位置随机重跑又过了。原因这套框架包含多个 32 位目标文件并行编译时每个编译器进程都会占 200~400MB 内存4GB 内存的机器上-j8很容易触顶。另外Makefile 的子目标之间如果缺少依赖关系并行时会出现头文件还没生成就被引用的竞态。这类错误和编译顺序有关但不是必然的所以最让人迷惑。解决物理机或云主机内存不足的强制用make -j1或-j2。不要为这几分钟时间折腾内存串行编译在 NEMU 这种小工程上通常两三分钟就完了。如果想保留并行可以只对navy-apps层使用-j4因为它是在前几层已经构建完的基础上才跑的竞争风险低。这个坑最迷惑人的地方是它今天能过明天不能过遇到这种情况先看系统日志是不是有内存不足OOM记录。如果确认是 Makefile 竞态就在顶层 Makefile 的依赖声明里给每个子目标加order-only依赖让nexus-am的头文件生成在nanos-lite编译前完成。5. 进阶实验给 NEMU 加一条指令并用 trace 对拍验证正确性当整条链路跑通之后我推荐的第一个扩展实验不是加系统调用而是给 NEMU 增加一条指令。因为指令是执行流的最底层最容易验证也最容易让你确认我到底有没有看懂执行流程。以 x86 的mul指令为例它的 opcode 是F7 /4含义是用EAX与r/m32做无符号乘法结果低 32 位存EAX高 32 位存EDX。实现步骤先在 opcode 表里注册F7的分组group 4然后写处理函数。// nemu/src/cpu/exec.c 中新增的 mul 处理函数 static void exec_mul(uint32_t dest) { uint64_t t (uint64_t)R_EAX * (uint32_t)dest; R_EAX (uint32_t)(t 0xffffffffu); R_EDX (uint32_t)(t 32); // 教材中的 mul 指令会更新 CF 和 OF cpu.eflags.CF cpu.eflags.OF (t 32 ? 1 : 0); } // 在 opcode_table 的 F7 条目中把 group 4 关联到 exec_mul参数说明R_EAX和R_EDX是 NEMU 寄存器数组的宏用寄存器别名而非直接操作cpu.gpr[0]可读性更高。强制类型转换保证了EAX * dest以 64 位中间结果计算避免 32 位溢出后高 32 位丢失。cpu.eflags里只更新CF/OF其他标志位按 x86 手册不受mul影响。写完后验证方法尤其重要。我一般的顺序是先用make -j1重编 NEMU再跑自带测试程序并生成修复前的 trace 日志然后重新生成修复后的 trace 日志用 Python 对拍。注意 trace 日志要记录每条指令的地址、编码和 CPU 状态变化这样才能发现指令执行了但寄存器状态不对的情况。# 对比两个 trace 文件判断指令执行是否一致 import sys mismatch 0 with open(sys.argv[1]) as f1, open(sys.argv[2]) as f2: for line1, line2 in zip(f1, f2): if line1.split()[1:] ! line2.split()[1:]: mismatch 1 if mismatch 5: print(Mismatch at line, mismatch) print( ref:, line1) print( ours:, line2) if mismatch 0: print(PASS) else: print(TOTAL MISMATCH:, mismatch)最后再说一个验证脚本本身的技巧故意把实现改错比如把R_EDX写成恒为 0然后重新对拍脚本应该迅速报错。这一步能确认你的对拍脚本不是永远通过的摆设。我每次写自动化验证都会先制造一个已知错误看到失败再修回来这是防止测试脚本把自己的 bug 当正确行为的后悔药。如果你已经完成了这条指令实验我建议顺着这个思路继续做三件事第一把 NanosLite 改成时间片轮转调度让两个应用交替运行观察CTE的上下文切换需要保存哪些寄存器第二在 NavyApps 里加入自己的一个小游戏用AM_INPUT_*读键盘体验从按键到应用层事件的全路径第三把 NEMU 的指令 trace 和 NanosLite 的printf输出放到同一个时间轴上你就能看到一条printf到底经过了多少条模拟指令。这种亲手把黑匣子打开的习惯是我做完整个课程实验后最大的收获。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号