把 8051 编译器移植到鸿蒙PC:SDCC 4.6.0 高难度适配复盘
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
项目源码(AtomGit 仓库):https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos
把 8051 编译器搬到鸿蒙PC:SDCC 4.6.0 高难度适配复盘
原创
一句话概括:这次适配不是"换个编译器再编一遍",而是把一个内嵌了整棵裁剪版 GCC 12.1.0 构建树的编译器工程,在鸿蒙PC 的 HMDFS 文件系统上完整跑通——最终 75 个目标架构库全部编译通过。配方位于
archives/s/sdcc/4.6.0,对应 PR !5660,已于 2026-08-20 合入OpenHarmonyPCDeveloper/build_in_harmonyos主线。
1 背景

图1 PR !5660 复审通过并合入主线(开源鸿蒙PC社区 build_in_harmonyos 仓库)
SDCC(Small Device C Compiler)是面向 8/16 位单片机的开源 C 编译器,支持 8051、Z80、STM8、mos6502、PIC、HC08、PDK 等 20 多个目标架构。在嵌入式开发里,它常被用来替代厂商的私有工具链——很多开源固件和教学项目直接依赖它产出的 .ihx 固件。
把它搬上鸿蒙PC 的意义,不在于"多了一个能跑的编译器",而在于嵌入式工具链在 OpenHarmony 生态里有了一个可复用的底座:这类编译器是固件开发链的入口,一旦它能在鸿蒙PC 上稳定构建与运行,后续的烧录、调试、仿真工具就有条件逐步接过来。
从工程难度看,SDCC 有两个特点让它区别于普通的 C/C++ 库:
- 它是一个编译器,验证方式不是"库能被链接",而是"能否真的把源码编译成目标固件";
- 它的发布源码里内嵌了一棵裁剪过的 GCC 12.1.0 构建树(
support/cpp,用于预处理)。这棵树在 sdist 打包时被裁掉了若干关键文件,只有真正把make跑到那一步才会暴露。
本次适配的成果:
| 项目 | 结果 |
|---|---|
| 全架构 device lib 编译 | 75/75 通过(20+ 目标架构全部产出 .lib) |
| 消费者冒烟编译 | 1/1 通过(8051 源码 → .ihx 固件) |
| 依赖解析 | bison / boost / flex / zlib 全部从 ohpcd 制品仓解析 |
| 版本输出 | SDCC : ... TD- 4.6.0 #16555 (Linux) |
2 上游假设与鸿蒙PC 环境的差异
SDCC 的构建假设来自常规 Linux 桌面环境,而鸿蒙PC 在四个层面与它有实质差异。源码树放进真机跑第一轮构建后,实际问题集中在下面几处:
| 差异点 | 上游假设 | 鸿蒙PC 实际情况 |
|---|---|---|
| 平台探测 | config.guess 认识目标平台 | 2018 版 config.guess 不识别 OHOS,configure 直接失败 |
| 文件系统 | 常规 POSIX 文件系统 | HMDFS(/storage 挂载)上 getcwd 失败、临时目录权限受限 |
| 依赖声明 | glibc 头文件声明齐全 | musl 未声明 pthread_setcancelstate,boost 编译报错 |
| 可选组件 | gputils 可用(PIC 工具链) | 制品仓没有 gputils,PIC 端口无法构建 |
| 二进制可执行性 | strip 后可正常执行 | strip 后的 ELF 报 Operation not permitted(代码签名体系) |
| 源码完整性 | 内嵌 GCC 树完整 | sdist 裁掉了 6 处关键文件/目录,编译到那步才报错 |
这些差异单个都不算太难,麻烦在于它们会在一次构建里同时出现,而且互相纠缠——尤其文件系统与源码完整性这两类,会分别把构建链在不同阶段打断。
3 适配流程
这次适配在 AtomCode 上完成,流程本身也是可复用的:先侦察确认源码地址与校验值 → 写 Conan 配方和平台补丁 → 真机跑构建 → 失败后诊断、修复、复核。
回头看,这套流程里最关键的一条是 “先取证,再动手”:
- 每个问题必须能看到旧状态失败、新状态通过的对照才算闭环,不靠"看起来应该修好了";
- 禁止吞错误、禁止放宽容差、禁止跳过用例;
- 修之前先确认上游是否已有修复,避免重复造轮子或掩盖真实根因。
也正因为这条,适配中复用了社区知识库里的既有结论(如 E007 config.guess 绕过、E011 HMDFS 权限处理、以及针对 hmdfs 脚本执行与签名体系的既有条目),把精力集中在知识库尚未覆盖的部分——也就是下面两节的核心难点。
4 核心难点一:HMDFS 上的 getcwd 与权限
4.1 config.status 的临时目录被拒
首个拦路虎出现在 configure 阶段。autoconf 生成的 config.status 会这样创建临时目录来写中间文件:
tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null`
在 HMDFS 上,用 umask 077 建出的目录后续写入被拒:
./config.status: can't create ./confXXXXXX/subs1.awk: Permission denied
更麻烦的是,SDCC 有多个递归 autotools 子工程(support/cpp、sim/ucsim 等),每个子工程都有自己的 configure / config.status——只改顶层没有用。因此适配脚本采用递归遍历 + 精确替换,把临时目录改到 $TMPDIR 下、并把 umask 放宽:
# 递归 patch 每一个 configure:临时目录改到 $TMPDIR,umask 077 → 022
s2 = s.replace('mktemp -d "./confXXXXXX"',
'mktemp -d "$TMPDIR/confXXXXXX"')
s2 = s.replace("tmp=$TMPDIR/conf$$-$RANDOM")
s2 = s.replace("(umask 077 && mktemp -d",
"(umask 022 && mktemp -d")
4.2 真正的硬骨头:make 子进程 getcwd 失败
上面那步解决后 make 跑起来了,但在嵌套子项目处再次中断:
getcwd: cannot access parent directories: Not supported
原因是 SDCC 顶层 Makefile 用递归方式驱动子项目:
for lib in $(SDCC_LIBS); do $(MAKE) -C $$lib; done
$(MAKE) -C X 不会更新 PWD 环境变量,而 HMDFS 上子进程随后调用 getcwd 就会失败,导致子目录里的 configure 找不到。
这里有两个处理,缺一不可。
① 整个构建迁移到非 HMDFS 分区。 实测 /data 分区上 pwd / cd / getcwd / chmod 全部正常。适配脚本先在候选目录中探测一个可写位置,再把源码整树复制过去构建:
work_root = None
for cand in ("/data/storage/el2/base/cache", "/data/cache",
self.build_folder):
try:
os.makedirs(cand, exist_ok=True)
probe = os.path.join(cand, ".sdcc-w-probe")
with open(probe, "w") as _f:
_f.write("x")
os.remove(probe)
work_root = cand
break
except OSError:
continue
if not work_root:
raise ConanException("no writable non-hmdfs work dir for sdcc")
work = os.path.join(work_root,
f"sdcc-{os.getpid()}-{int(time.time() * 1000)}")
shutil.copytree(self.source_folder, work, symlinks=True)
# copytree 保留了 HMDFS 源权限(含 other 无读权限的目录),
# 在 /data 上 chmod 生效,递归放开让 make 子进程能 stat 父目录链
for root, _dirs, _files in os.walk(work):
try:
os.chmod(root, 0o777)
except OSError:
pass
② 把 $(MAKE) -C X 改成子 shell 形式。 因为 bash cd 会更新 PWD 环境变量,子 make 继承到正确的 PWD,就不再触发 getcwd:
("for lib in $(SDCC_LIBS); do $(MAKE) -C $$lib; done",
"for lib in $(SDCC_LIBS); do (cd $$lib && $(MAKE)); done"),
("$(foreach as, $(SDCC_AS), $(MAKE) -C $(as) &&)",
"$(foreach as, $(SDCC_AS), (cd $(as) && $(MAKE)) &&)"),
针对行尾无参形式,再用正则统一替换。这里有个容易踩的细节:行尾要用 (\s*)$ 锚定,否则会把 foreach / for 那两行一并误伤(它们同样含有 $(MAKE) -C):
# 行尾用 (\s*)$ 锚定,避免误伤 foreach/for 行
ms = re.sub(r'\$\(MAKE\) -C (\S+)(\s*)$',
r'(cd \1 && $(MAKE))', ms, flags=re.M)
一个反直觉的教训:不要手动
export PWD。预设的PWD若与 make 实际工作目录不一致,bash 子进程反而会重新getcwd,又触发 Permission denied。只保留umask 022+cd即可。
5 核心难点二:内嵌 GCC 树的六处缺失
support/cpp 是 GCC 12.1.0 的裁剪版,SDCC 用它做预处理。sdist 打包时裁掉了一批文件,必须逐个补回——否则编译到那一步才报错。这次共处理了六处:
5.1 config/aarch64/ 目录整体缺失
该目录只有 dummy / i386 / riscv,host 若是 aarch64,cc1 编译必然引用 config/aarch64/*.def。补空桩文件即可(sdcpp 是预处理器,扩展选项内容无关紧要,只需文件存在):
aarch64_dir = os.path.join(gcc_dir, "config", "aarch64")
os.makedirs(aarch64_dir, exist_ok=True)
for _def in ("aarch64-arches.def", "aarch64-cores.def",
"aarch64-option-extensions.def"):
_p = os.path.join(aarch64_dir, _def)
if not os.path.isfile(_p):
open(_p, "w").write(
f"/* minimal stub for {_def} — SDCC sdist trims "
"aarch64 support */\n")
5.2 LIBIBERTY 路径指向不存在的目录
GCC Makefile 默认指向 ../libiberty/,而 support/cpp 下没有该目录。改为指向顶层已构建好的那份:
s2 = s.replace(
"LIBIBERTY = ../libiberty/$(LIBIBERTY_PICDIR)/libiberty.a",
"LIBIBERTY = ../../sdbinutils/libiberty/libiberty.a")
顺带纠正一个此前的误判:一度以为
libiberty源码不在 sdist 里、打算--disable-sdbinutils绕开;实际核对后确认源码就在 sdist 中,于是保留 sdbinutils 构建,只把 GCC 子项目的LIBIBERTY指过去。误判必须在动手前纠正,否则会引入不必要的功能裁剪。
5.3 bversion.h 模板被裁掉
configure 不再生成它,而 gcc.o 编译依赖 GCC_VERSION 数字宏,需手写:
open(bversion, "w").write(
"/* bversion.h — normally generated by configure;\n"
" SDCC sdist trims the template. */\n"
"#define GCC_VERSION 120100\n"
"#define GCC_VERSION_FULL \"12.1.0\"\n")
5.4 all-tree.def 需先生成
它是 make 目标,但被其他目标依赖,需先单独跑一次:
self.run(
f"/data/service/hnp/bin/bash -c \"umask 022 && "
f"cd '{gcc_dir}' && "
f"make all-tree.def -j2 TMPDIR='{tmpdir}'\"",
cwd=gcc_dir)
5.5 补丁顺序有依赖
gcc/Makefile 是由 configure-gcc 目标生成的。所以必须先触发 configure-gcc,再打 LIBIBERTY 补丁——顺序反了,补丁就打在空气上(文件还不存在)。这类"补丁依赖生成顺序"的问题在嵌套构建树里很常见。
5.6 libexec 必须随包
cc1 落在 libexec/sdcc/<target>/<ver>/cc1,SDCC 驱动运行期需要它。漏打包会直接报 cannot execute 'cc1':
# bin/share/lib + libexec(cc1 在 libexec 下,驱动运行期需要)
for entry in ("bin", "share", "lib", "libexec"):
s = os.path.join(src, entry)
if os.path.isdir(s):
shutil.copytree(s, os.path.join(self.package_folder, entry))
6 其余差异
6.1 平台探测绕过
2018 年的 config.guess 不认识 OHOS。按知识库既有做法(E007),直接显式传 triplet 绕过探测:
bld = "--build=aarch64-unknown-linux-gnu --host=aarch64-unknown-linux-gnu"
6.2 boost 的 musl 声明缺失
OHOS musl 未声明 pthread_setcancelstate,而 boost 的 sp_thread_sleep.hpp 用了它。做法是复制该头文件到工作树内打补丁(补 extern 声明),并用 CPPFLAGS 让编译器优先找到它:
decl = ("\n// OHOS musl: pthread_setcancelstate not declared in "
"<pthread.h> — declare it\n"
"#if defined(BOOST_HAS_PTHREADS) && !defined(__ANDROID__)\n"
"extern \"C\" int pthread_setcancelstate(int __state, "
"int *__oldstate);\n#endif\n")
cppflags = f"-I{patch_dir} -I{os.path.join(boost_inc, 'include')} -D_GNU_SOURCE"
6.3 关闭缺依赖的可选端口
制品仓没有 gputils(PIC 工具链),PIC 端口无法构建,按需关闭:
opts = "--disable-pic14-port --disable-pic16-port --disable-nls " \
f"--without-ccache --enable-host-shared --prefix={install_prefix}"
6.4 功能取舍:install 阶段禁用 strip
这是本次适配中一处明确的功能取舍,需要写清楚而不是含糊带过。
SDCC 的 make install 默认会 strip 安装的二进制。但在鸿蒙PC 上,strip 后的 ELF 执行会报:
Operation not permitted
这与鸿蒙的代码签名体系有关(未签名的、被改造过的 ELF 无法执行)。处理方式是在 install 时把 STRIP 指向一个空操作:
self.run(
f"/data/service/hnp/bin/bash -c \"umask 022 && "
f"cd '{work}' && "
f"make install STRIP=/bin/true -j2 TMPDIR='{tmpdir}'\"",
cwd=work)
代价要说清楚:产物体积会比常规发行版偏大(二进制未 strip)。这是为了优先保证"能构建、能运行";体积优化留待后续有签名方案时再处理。
7 真机运行与复现
适配完成后,在同一台设备上做了一次端到端复现。注意 SDCC 的一个特点:编译成功时不打印任何输出(Unix 惯例),所以重点看产物:
$ .../bin/sdcc --version
SDCC : mcs51/z80/z180/r2k/r2ka/r3ka/r4k/r5k/r6k/sm83/tlcs90/ez80/z80n/r800/ds390/
TININative/ds400/hc08/s08/stm8/pdk13/pdk14/pdk15/mos6502/mos65c02/f8/f8l
TD- 4.6.0 #16555 (Linux)
published under GNU General Public License (GPL)
$ .../bin/sdcc -mmcs51 t.c
(无输出即成功)
$ ls -l t.ihx
-rw-rw-r-- 1 ... 552 t.ihx
那一行 SDCC : mcs51/z80/z180/... 列出了它支持的全部目标架构——视觉上就能确认这是一个完整可用的跨架构编译器,而不是被裁剪过的残缺构建。
test_package 的消费者测试做的事是:把包复制到非 HMDFS 分区,再用打包后的编译器真实编译一个最小 8051 程序,并断言 Intel HEX 产物生成:
src = ("/* minimal 8051 target program */\n"
"void main(void) { volatile unsigned char i; "
"for (i = 0; i < 10; i++) ; }\n")
r = subprocess.run([w_sdcc, "--std-c11", "-mmcs51", c_file],
capture_output=True, text=True,
cwd=work, # sdcc 把 .ihx/.rel 输出到当前目录
env=dict(os.environ,
PATH=os.path.join(work, "bin") + os.pathsep
+ os.environ.get("PATH", "")))
ihx = os.path.join(work, "t8051.ihx")
if not os.path.isfile(ihx):
raise AssertionError(f"sdcc output .ihx missing: {ihx}")
测试输出:
OK: sdcc 4.6.0 real API ok (8051 compile -> .ihx)
这里还有一个 HMDFS 的细节值得记下来。 测试时不能直接在包目录里跑编译器——包位于 /storage(HMDFS),posix_spawn 打包好的二进制会报 Operation not permitted。所以测试脚本先把整个包复制到非 HMDFS 分区,再在那里执行。
另外 sdcpp 找 cc1 用的是编译期写死的 STANDARD_LIBEXEC_PREFIX 绝对路径,包被复制走后该路径失效。解法是把 cc1 一并复制到 bin/(与 sdcpp 同目录),借助 PATH 查找:
cc1_src = None
for root, _dirs, files in os.walk(os.path.join(work, "libexec")):
if "cc1" in files:
cc1_src = os.path.join(root, "cc1")
break
if cc1_src:
shutil.copy2(cc1_src, os.path.join(work, "bin", "cc1"))

图2 HUAWEI MateBook Pro(鸿蒙PC)真机运行 sdcc
如果你手上有配置了社区适配环境的鸿蒙PC 设备,可以直接验证:
conan remote add ohpcd https://conan.cnb.cool/OpenHarmonyPCDeveloper/Conan/-/packages/
conan list "sdcc/*" -r=ohpcd # 查看可用版本与官方构建产物
本次验证的边界(如实声明):覆盖的是工程构建 + 全架构库编译(75/75)+ 8051 冒烟编译。编译器在更复杂工程上的等价性(例如大型 C 项目的完整编译产物比对)未作为本次验收项。
8 小结
回头看,这次适配的麻烦不在"改了多少代码",而在于两个层面的问题互相纠缠:
- 文件系统层面:HMDFS 的
getcwd、权限、脚本执行限制,会把 autotools 的递归构建链在不同阶段打断——解法是迁移到非 HMDFS 分区 + 把$(MAKE) -C改为子 shell 形式; - 源码完整性层面:内嵌 GCC 树被 sdist 裁剪,缺失文件要到构建那一步才暴露——解法是逐处补齐桩文件、修正路径、补生成目标,并注意补丁与生成的先后顺序。
两类问题叠加,构成了这次"高难度"的实质。可复用的方法有几条:
- 先取证再动手——每个修复都要能看到旧状态失败、新状态通过的对照;
- 误判要在动手前纠正——本次
libiberty一度被误判为"不在 sdist",核对后发现源码就在其中,避免了不必要的功能裁剪; - 修复保持最小改动——桩文件只补必需项,不引入额外依赖;
- 功能取舍要写明代价——禁用 strip 是明确取舍,产物体积偏大这点必须如实说明,而不是含糊掩盖。
最终成果:全架构 75/75 编译通过,消费者冒烟 1/1 通过。SDCC 4.6.0 已能在鸿蒙PC 完成完整 Conan 构建,并对主要产物做了真实功能检查。
后续值得补的方向有两个:一是配合鸿蒙签名方案恢复 strip 以优化体积,二是用更复杂的目标工程验证编译器输出等价性。
编译器是嵌入式生态的地基。SDCC 在鸿蒙PC 上跑通全部 20+ 目标架构,意味着 8051 / Z80 / STM8 这条嵌入式工具链在 OpenHarmony 上有了可用的入口。
参考文献
[1] SDCC — Small Device C Compiler 官方网站. https://sdcc.sourceforge.net/
[2] SDCC User Guide / Manual. https://sdcc.sourceforge.net/doc/sdccman.pdf
[3] OpenHarmonyPCDeveloper/build_in_harmonyos — SDCC 4.6.0 适配配方(archives/s/sdcc/4.6.0),PR !5660. https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
项目源码(AtomGit 仓库):https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos
更多推荐


所有评论(0)