简介:这份资源面向需要在Android平台上挂载NTFS格式存储设备的开发者与系统移植人员,提供ntfs-3g的完整移植源码,解决TF卡、U盘及SATA硬盘在Android系统中无法识别NTFS分区的问题。压缩包共300个文件,约1.39MB,以73个C源文件和61个头文件为核心实现,辅以42个plo、34个po本地化文件、28个in模板及多份Makefile、am构建脚本,另含configure、libtool等自动构建工具链文件,整体结构贴近原生ntfs-3g工程,便于直接放入external目录执行mm编译。编译成功后可手动执行挂载命令,也可将Ntfs.cpp、Ntfs.h加入system/vold目录实现开机自动挂载。目前已有1835人学习下载,适合具备一定Android源码编译与vold机制基础的开发者参考,能帮助快速完成NTFS读写支持移植与调试。
1. 从一次编译翻车说起:为什么 Android 上要移植 ntfs-3g
去年帮一个做车载多媒体的团队调一块外接硬盘,Android 11 的板子插上 NTFS 分区后系统只读挂载,写入直接报EACCES。他们试过mount -t ntfs,内核里那个老掉牙的只读驱动根本不支持写;又试了exfat,格式对不上。最后绕回 ntfs-3g——这个在 Linux 桌面端跑了十几年的用户态 NTFS 读写方案,才是 Android 上真正能落地的选择。
问题在于,Android 的编译体系和标准 Linux 发行版差别太大。你从官网拉一份 ntfs-3g 源码,./configure && make那套在 AOSP 源码树里根本走不通,缺libfuse、缺pkg-config、NDK 工具链路径对不上,报错能刷满一屏。这份移植源码的价值就在于:它已经把 Android.mk 写好了,放进 AOSP 源码树对应目录,mm就能编出可执行文件和依赖库,省掉自己写交叉编译脚本的折腾。
适合谁看:手里有 Android 源码树、需要让设备读写 NTFS 移动硬盘或 U 盘的系统开发;以及想搞清楚 ntfs-3g 在 Android 上到底依赖什么、怎么编、怎么挂载的运维和嵌入式工程师。下面按我实际走通的顺序,从环境准备一路写到挂载验证。
2. 环境准备与源码落位:AOSP 树里该放哪、依赖什么
2.1 版本与工具链确认
这套移植源码对编译环境有明确要求,不是随便一个 NDK 就能编。我实测通过的组合是:
| 项目 | 要求 | 说明 |
|---|---|---|
| AOSP 版本 | Android 8.0 ~ 13 | 低版本 Android.mk 语法兼容性更好 |
| 编译方式 | 源码树内mm | 依赖 AOSP 的 build 系统 |
| 工具链 | AOSP 自带 clang / gcc | 不用单独装 NDK |
| 依赖库 | libfuse(源码内已含) | ntfs-3g 的用户态核心依赖 |
| 目标架构 | arm / arm64 | 按你的板子选 |
先确认你的源码树能正常编译。进到 AOSP 根目录,执行:
source build/envsetup.sh lunch aosp_arm64-userdebugsource这步是把mm、mmm、lunch这些命令注入当前 shell,不执行后面全白搭。lunch选的目标要和你板子的架构一致,选错了编出来的二进制跑不起来。我一般会先make -j8随便编一个模块确认工具链没问题,再动 ntfs-3g。
2.2 源码目录落位
这份移植源码的目录结构是围绕 AOSP 的external/目录设计的。常见做法是把它整体放到:
# 假设源码包解压后目录名为 ntfs-3g-android cp -r ntfs-3g-android $AOSP_ROOT/external/ntfs-3g放external/下的原因是 AOSP 的 build 系统会自动扫描这个目录里的Android.mk,mm命令才能在当前目录直接生效。如果你放到别的路径,得用mmm指定完整路径,而且LOCAL_PATH的相对关系容易出错。
放好之后检查目录里有没有这几个关键文件:
ls external/ntfs-3g/ # 应该能看到: # Android.mk <- 编译入口 # libfuse/ <- fuse 依赖 # src/ <- ntfs-3g 主源码 # include/ <- 头文件Android.mk是这套移植的核心,它定义了BUILD_EXECUTABLE(编 ntfs-3g 主程序)和BUILD_SHARED_LIBRARY(编 libntfs-3g 和 libfuse)。如果这个文件缺失或者路径写错,mm会直接报No Android.mk,那就得回头确认源码包完整性。
2.3 依赖关系梳理
ntfs-3g 在 Android 上跑起来,运行时依赖三样东西:
- libfuse:用户态文件系统框架,ntfs-3g 通过它和内核 FUSE 模块通信。Android 内核一般已编入
CONFIG_FUSE_FS,但用户态库得自己带。 - libntfs-3g:ntfs-3g 自己的核心库,处理 NTFS 元数据读写。
- libc / libdl:系统库,AOSP 自带。
编译时Android.mk里会用LOCAL_SHARED_LIBRARIES把 libfuse 和 libntfs-3g 链进去。你不需要手动装任何东西,但得确认内核配置里 FUSE 是开着的:
# 在板子上执行,或看内核 config zcat /proc/config.gz | grep FUSE # 期望输出: # CONFIG_FUSE_FS=y如果输出是# CONFIG_FUSE_FS is not set,那 ntfs-3g 编出来也挂不上,得先重新编内核。这是最容易忽略的前置条件,我踩过一次,编了半天发现内核没开 FUSE。
3. mm 编译实操:从 Android.mk 到可执行文件
3.1 进入目录并执行 mm
环境确认无误后,编译本身只有一条命令:
cd $AOSP_ROOT/external/ntfs-3g mm -j8mm是 AOSP build 系统提供的命令,作用是在当前目录下查找Android.mk并编译,自动处理依赖。-j8是并行编译线程数,按你机器核数调整,一般设成 CPU 核数的 1~2 倍。
编译过程会依次产出:
# 中间产物路径 out/target/product/<你的设备>/obj/EXECUTABLES/ntfs-3g_intermediates/ out/target/product/<你的设备>/obj/SHARED_LIBRARIES/libntfs-3g_intermediates/ out/target/product/<你的设备>/obj/SHARED_LIBRARIES/libfuse_intermediates/ # 最终产物 out/target/product/<你的设备>/system/bin/ntfs-3g out/target/product/<你的设备>/system/lib64/libntfs-3g.so out/target/product/<你的设备>/system/lib64/libfuse.so<你的设备>就是你lunch时选的 target 名。产物落在system/bin和system/lib64下,说明Android.mk里的LOCAL_MODULE_PATH配置正确,后续打包进系统镜像就能直接用。
3.2 Android.mk 关键参数解读
这份移植源码的Android.mk里有几个参数值得单独说,改错了编译能过但运行会崩:
# ntfs-3g 主程序定义 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := ntfs-3g LOCAL_SRC_FILES := src/ntfs-3g.c src/... # 主源码列表 LOCAL_SHARED_LIBRARIES := libntfs-3g libfuse LOCAL_CFLAGS := -DHAVE_CONFIG_H -DANDROID # 关键宏 LOCAL_MODULE_PATH := $(TARGET_OUT)/bin # 安装到 system/bin include $(BUILD_EXECUTABLE)-DANDROID这个宏是移植时加的,源码里有些#ifdef ANDROID分支专门处理 Android 的路径和权限差异,不加会走到 Linux 桌面端的逻辑,挂载点路径就错了。LOCAL_MODULE_PATH决定产物最终进哪个分区,写$(TARGET_OUT)/bin就是system/bin,如果你想让它在vendor/bin,改成$(TARGET_OUT_VENDOR)/bin。
LOCAL_SHARED_LIBRARIES里的libfuse和libntfs-3g必须在同一个Android.mk或同目录的其它Android.mk里有定义,否则链接阶段报undefined reference。这份源码把三个模块写在一个文件里,顺序上先编库再编可执行文件,依赖关系是自洽的。
3.3 编译产物验证
编完之后别急着刷机,先在本地验证产物架构和依赖:
# 确认架构 file out/target/product/<设备>/system/bin/ntfs-3g # 期望:ELF 64-bit LSB executable, ARM aarch64 # 确认动态依赖 readelf -d out/target/product/<设备>/system/bin/ntfs-3g | grep NEEDED # 期望看到: # libntfs-3g.so # libfuse.so # libc.so # libdl.so如果file显示的是 x86 架构,说明lunch选错了 target,回头重新lunch再mm。如果readelf里缺libfuse.so,检查Android.mk的LOCAL_SHARED_LIBRARIES是否包含它,以及 libfuse 模块是否编译成功。
4. 避坑与常见问题:编译和挂载阶段的血泪记录
4.1 报错fuse: device not found
现象:ntfs-3g 编出来推到板子上,执行挂载命令报fuse: device not found, try 'modprobe fuse' first。
原因:内核没有编入 FUSE 模块,或者/dev/fuse设备节点不存在。Android 常见的内核配置里 FUSE 可能是模块形式(=m)而不是内建(=y),模块没加载就没有设备节点。
解决:先确认内核配置CONFIG_FUSE_FS=y或=m。如果是=m,需要把fuse.ko推到板子并insmod。更彻底的做法是改内核 config 改成=y重新编内核。另外检查/dev/fuse权限,ntfs-3g 需要读写这个设备:
ls -l /dev/fuse # 期望:crw-rw-rw- 1 root root 10, 229 ... # 如果权限不对: chmod 666 /dev/fuse4.2 编译报undefined reference to 'fuse_...'
现象:mm到链接阶段报一堆undefined reference to 'fuse_mount'、fuse_loop之类的错误。
原因:libfuse 没有被正确链接。可能是Android.mk里LOCAL_SHARED_LIBRARIES漏了libfuse,或者 libfuse 模块本身编译失败但被忽略了。
解决:先单独编 libfuse 确认它能过:
cd external/ntfs-3g/libfuse mm -j8如果 libfuse 编不过,看它的Android.mk里LOCAL_SRC_FILES是否列全了.c文件。libfuse 有些版本依赖libdl,需要在LOCAL_SHARED_LIBRARIES里加上libdl。确认库编出来后,再回 ntfs-3g 目录重新mm。
4.3 挂载成功但写入报Read-only file system
现象:mount -t ntfs-3g /dev/block/sda1 /mnt/usb挂上了,但touch新文件报只读。
原因:ntfs-3g 默认可能以只读模式挂载,或者 NTFS 分区有脏标志(Windows 没正常卸载留下的)。另外 Android 的 SELinux 策略可能阻止了写入。
解决:挂载时显式加读写选项:
mount -t ntfs-3g -o rw,umask=0000 /dev/block/sda1 /mnt/usbumask=0000让所有用户可读写。如果是脏标志问题,用ntfsfix清一下:
ntfsfix /dev/block/sda1SELinux 问题看dmesg | grep avc,有拒绝日志就说明需要加策略或临时setenforce 0验证。
4.4mm报No Android.mk found
现象:在 ntfs-3g 目录执行mm,提示找不到Android.mk。
原因:目录放错位置,或者Android.mk文件名大小写不对(AOSP 严格要求大写Android.mk)。
解决:确认当前目录下有Android.mk,且路径在$AOSP_ROOT/external/下。如果放在其它位置,用mmm加完整路径:
mmm $AOSP_ROOT/path/to/ntfs-3g另外确认source build/envsetup.sh和lunch都执行过了,否则mm命令本身就不存在。
4.5 推送到板子后执行报not found
现象:adb push ntfs-3g /system/bin/之后,adb shell ntfs-3g报not found。
原因:动态链接库没推过去,或者推的位置不对。Android 的 linker 在system/lib64找 64 位库,system/lib找 32 位库。
解决:把libntfs-3g.so和libfuse.so一起推到对应目录:
adb push libntfs-3g.so /system/lib64/ adb push libfuse.so /system/lib64/ adb shell chmod 644 /system/lib64/libntfs-3g.so adb shell chmod 644 /system/lib64/libfuse.so如果system分区只读,先adb remount。推完用ldd或readelf在板子上确认依赖都能找到。
5. 装完怎么验证跑通:挂载、读写、卸载一条龙
编完推完只是第一步,真正验证这套移植能不能用,得走一遍完整的挂载读写流程。我一般会准备一个 NTFS 格式的 U 盘或镜像文件,按下面顺序过。
5.1 用镜像文件做无硬件验证
手头没 U 盘的时候,用镜像文件也能验证,避免反复插拔:
# 在板子上创建一个 64M 的 NTFS 镜像 dd if=/dev/zero of=/data/local/tmp/test.img bs=1M count=64 # 用 mkntfs 格式化(如果源码里带了 mkntfs) mkntfs -F /data/local/tmp/test.img # 挂载 mkdir -p /mnt/test ntfs-3g /data/local/tmp/test.img /mnt/test-F是强制格式化,因为镜像文件不是块设备,mkntfs 默认会拒绝。挂载成功后mount | grep ntfs应该能看到/mnt/test这一条。
5.2 读写验证
挂载点有了,接下来验证读写是否真的可用:
# 写入测试 echo "ntfs-3g android test" > /mnt/test/hello.txt # 读回 cat /mnt/test/hello.txt # 期望输出:ntfs-3g android test # 创建目录和大文件 mkdir /mnt/test/dir1 dd if=/dev/zero of=/mnt/test/dir1/big.bin bs=1M count=32 # 确认文件大小 ls -l /mnt/test/dir1/big.bin写入成功且读回内容一致,说明 ntfs-3g 的读写路径是通的。如果写入报错,回看第 4 章的只读问题排查。大文件测试是为了验证超过内存缓存的写入场景,小文件能过不代表大文件没问题。
5.3 卸载与数据一致性
验证完别忘了正常卸载,这一步能暴露缓存没刷盘的问题:
umount /mnt/test # 如果报 device is busy: fuser -m /mnt/test # 看谁占着 # 或者强制卸载 umount -l /mnt/test卸载后重新挂载,确认之前写的文件还在:
ntfs-3g /data/local/tmp/test.img /mnt/test ls /mnt/test/ # 应该能看到 hello.txt 和 dir1这一步是验证数据真的落盘了,而不是只在页缓存里。我遇到过卸载时没报错但重新挂载文件丢失的情况,原因是 ntfs-3g 的日志回放没走完,后来每次验证都强制走一遍「挂载→写→卸载→重挂→读」的完整循环。
5.4 验证清单
把上面的步骤整理成一张检查表,每次移植完新板子照着过一遍:
| 检查项 | 命令 | 通过标准 |
|---|---|---|
| 内核 FUSE | zcat /proc/config.gz | grep FUSE | CONFIG_FUSE_FS=y |
| 设备节点 | ls -l /dev/fuse | 存在且可读写 |
| 二进制架构 | file ntfs-3g | 匹配板子架构 |
| 动态依赖 | readelf -d ntfs-3g | 无 missing |
| 挂载 | ntfs-3g /dev/xxx /mnt | 无报错 |
| 写入 | echo test > /mnt/f | 成功 |
| 读回 | cat /mnt/f | 内容一致 |
| 卸载重挂 | umount && mount | 文件仍在 |
这张表我贴在工位上,每换一块板子就从头走一遍。有次偷懒跳过了卸载重挂这步,结果量产时发现异常断电后文件系统损坏,回头查就是日志没配好。从那以后我每次移植完 ntfs-3g,都强制走一遍完整的挂载读写卸载循环,确认数据落盘才敢往下走。希望这套流程能帮你少走点弯路。
本文还有配套的精品资源,点击获取