ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ARM开发板定制Ubuntu Base 20.04根文件系统:从chroot到镜像烧录全攻略

ARM开发板定制Ubuntu Base 20.04根文件系统:从chroot到镜像烧录全攻略 直接从“怎么给ARM开发板做个能用的根文件系统”说起。玩过开发板的人应该都有这种感觉官方给的SDK或者Buildroot镜像确实省事一烧录就能跑但真到调试应用、装第三方库、跑Qt程序的时候各种包缺失和限制就让人抓狂了。而Ubuntu Base 20.04这种方案本质上就是给你一块未经裁剪的Ubuntu地基拿来自己往上面盖房子自由度极高装软件直接用apt舒服得很。这篇文章要做的就是从头到尾带你把Ubuntu Base 20.04的rootfs剥出来、配置好、打包烧进ARM开发板里顺便把手冰凉凉踩过的坑都摆出来尤其是网络配置、Qt运行环境、中文显示这几个高频问题。我会先从方案选型讲起告诉你为什么选Ubuntu Base而不是别的然后一步步走通“下载-解压-挂载-配置-打包-烧录”全流程。整个过程中涉及到的chroot、交叉编译、镜像制作这些概念我会尽量用大白话解释清楚确保哪怕你之前只玩过树莓派、对ARM Linux生态不太熟也能跟着把系统跑起来。适合的对象很明确手里有i.MX6ULL、T113、RK3568之类ARM开发板不满足于官方预置系统、想自己做定制系统的朋友。1. 方案选型为什么偏偏是Ubuntu Base 20.04网上做根文件系统的教程五花八门有直接用BusyBox的、有Buildroot一条命令搞定的、有Yocto折腾到天荒地老的我最终选择Ubuntu Base 20.04完全是基于实际项目需求权衡出来的结果。1.1 各主流方案的优缺点对比先说BusyBox。它做出来的根文件系统极小跑起来占内存也就几十MB适合Flash空间非常紧张、硬件资源极其有限的场景。但代价是什么没有包管理机制想装个程序基本靠拷贝、交叉编译依赖关系得自己手工打理。你要是只想让它跑个静态编译的小程序没问题想跑个Python脚本、部署个数据库服务能把人逼疯。Buildroot比BusyBox进了一步它有自己的包管理make menuconfig选包能帮你把Python、Qt、SSH这些常见组件编进去。但它制作的rootfs本质上是“定制化产物”跟Ubuntu这类通用发行版不太一样——你要是临时想装个libgstreamer的某个子库Buildroot里没有这个包名就得自己写build脚本去编译整个生态极其封闭不适合快速迭代做应用开发。Yocto功能最强也最复杂它适合大批量产品化、需要对所有软件版本做严格追溯的项目。但说实话单人为了一块开发板从头配置整套Yocto环境光编一个基础镜像就可能花掉大半天中间各种BB文件、layer依赖能把新手直接劝退。Ubuntu Base 20.04则是另一条路——它本质上就是Ubuntu服务器版裁剪后的基础系统档案包rootfs tarball保留了完整的apt包管理机制和几乎完整的软件仓库。装软件一条apt install搞定依赖关系系统帮你维护想要什么库都能在源里找到ARM版。缺点就是体积比BusyBox大不少但对现在动辄几个GB的eMMC/SD卡来说这根本不算事。尤其是做带屏幕、跑Qt应用的开发板Ubuntu Base这种“标准Linux发行版式”的rootfs简直是为应用开发者量身定制的。1.2 选择20.04而非18.04或22.04的理由版本选择上20.04Focal Fossa是目前平衡性最好的一个。18.04太老GCC默认还是7.x面对一些较新的第三方库源码编译会比较痛苦22.04相比之下很新但部分ARM开发板的BSP板级支持包和内核模块还是基于老版本Ubuntu适配的直接套用容易遇到内核头文件、库版本不匹配的兼容问题。20.04的GCC 9.4、GLIBC 2.31实用性很强做交叉编译VSCode远程开发时的兼容性也好而且清华、中科大这些镜像站都还保留着完整的focal源稳定用几年没问题。2. 开工前准备镜像下载与宿主环境搭建方案定了后面就是各种准备工作。这一块看似简单但下载源的选择、宿主机的环境配置会直接影响后续步骤的顺畅度值得专门说细一点。2.1 Ubuntu Base 20.04镜像获取Ubuntu Base官方发布在cdimage.ubuntu.com但国内访问时快时慢建议直接用国内镜像站推荐清华TUNA和中科大USTC镜像。清华镜像https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cdimage/ubuntu-base/releases/20.04/release/中科大镜像https://mirrors.ustc.edu.cn/ubuntu-cdimage/ubuntu-base/releases/20.04/release/打开之后找文件名类似ubuntu-base-20.04.5-base-armhf.tar.gz的包这就是ARM 32位版。如果你的开发板是ARMv8 64位架构比如RK3568、树莓派4B跑64位系统就下ubuntu-base-20.04.5-base-arm64.tar.gz。下载完建议顺手校验一下SHA256防止文件损坏。注意ARM Cortex-A系列处理器型号众多但rootfs本身跟具体芯片型号没直接关系——它只区分32位armhf和64位arm64。只要你内核能跑起来rootfs的架构匹配正确就行。这就是rootfs“一次制作、多板复用”的根本原因。2.2 宿主机准备与依赖安装制作rootfs的过程需要在一个x86的Linux宿主机上进行用qemu-user静态模拟来实现在x86机器上执行ARM指令。宿主系统我建议用Ubuntu或者Debian其他发行版也行但以下命令以Ubuntu系为例sudo apt update sudo apt install -y qemu-user-static binfmt-support重点说一下qemu-user-static这个包它提供静态编译的qemu-arm和qemu-aarch64模拟器无需在宿主机上运行完整的ARM虚拟机只在执行ARM二进制文件时进行指令翻译。配合binfmt-support这个机制宿主机能自动识别ARM格式的可执行文件并用对应的qemu去解释执行它。这一步是整个“在x86上配置ARM rootfs”的核心没有它后面一进入chroot环境就会报Exec format error。再装几个辅助工具sudo apt install -y debootstrap parted dosfstoolsdebootstrap虽然我们不一定直接用但它内部的挂载流程值得参考parted和dosfstools用来给SD卡分区和格式化FAT文件系统。3. 核心实操从解压到chroot配置前期工作就绪下面进入整个流程最核心的部分。前面花了这么多篇幅讲方案真正动手起来其实步骤不多但每一步的次序和细节很讲究。3.1 解压rootfs与目录准备先建个工作目录把下载好的tar.gz包解压进去mkdir -p ~/arm-rootfs/ubuntu-base cd ~/arm-rootfs/ubuntu-base sudo tar -xzf ubuntu-base-20.04.5-base-armhf.tar.gz解压后应该能看到bin、etc、lib等标准Linux目录结构。有一点要提醒解压前先看一眼当前目录是否为空里面如果已有旧文件混在一起后面排查问题会非常痛苦。3.2 复制qemu模拟器并启用chroot接下来把我们刚才装的qemu静态模拟器复制到rootfs里这样chroot进去之后ARM架构的程序才能在这个“ARM环境”里被执行sudo cp /usr/bin/qemu-arm-static ~/arm-rootfs/ubuntu-base/usr/bin/复制完之后用file命令确认一下file ~/arm-rootfs/ubuntu-base/usr/bin/qemu-arm-static正常会输出包含“ARM”字样的信息。如果你是arm64的rootfs复制的是qemu-aarch64-static这一步别搞混。3.3 chroot前必须做的三件准备工作想要chroot之后网络通、源能用、DNS能解析下面三件事每一样都不能少。我见过不少教程让用户直接chroot进去就开始apt update然后报错问题几乎都出在这里。第一把宿主机的网络配置文件复制到rootfs的/etc/resolv.conf里sudo cp /etc/resolv.conf ~/arm-rootfs/ubuntu-base/etc/resolv.conf这个文件负责DNS解析不复制的话chroot内apt update会报“Temporary failure resolving”之类的DNS错误。第二挂载一些关键的虚拟文件系统sudo mount -t proc /proc ~/arm-rootfs/ubuntu-base/proc sudo mount -t sysfs /sys ~/arm-rootfs/ubuntu-base/sys sudo mount -o bind /dev ~/arm-rootfs/ubuntu-base/dev sudo mount -o bind /dev/pts ~/arm-rootfs/ubuntu-base/dev/pts/proc和/sys是内核与用户空间交互的虚拟文件系统不挂载进去很多命令会读取不到系统信息安装某些软件包时也会出错。/dev用bind方式挂载是为了让chroot环境能直接访问宿主机的主设备节点——当然这一步的操作只影响我们的chroot环境不会伤害宿主系统放心用。第三把apt的源换成国内镜像。Ubuntu Base包内的sources.list默认指向ports.ubuntu.com国内访问极慢建议换成清华源sudo tee ~/arm-rootfs/ubuntu-base/etc/apt/sources.list EOF deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal main restricted universe multiverse deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal main restricted universe multiverse EOF注意ARM架构的Ubuntu仓库和x86的不一样x86用的是archive.ubuntu.comARM必须用ports.ubuntu.com或镜像站的ubuntu-ports路径。用错源会导致404错误地址却长得跟对的一样非常坑。3.4 进入chroot并配置基础系统准备工作做齐就进入chroot环境了sudo chroot ~/arm-rootfs/ubuntu-base /bin/bash进去之后第一眼发现命令行提示符变成了I have no name!这是正常的——因为rootfs里还没设置主机名和用户信息不用慌。先初始化基础配置apt update apt install -y systemd systemd-sysv这里一定要先装systemd-sysv它会把systemd设置为init系统。很多教程没装这个包导致rootfs打包烧录之后内核启动找不到init、卡死在Kernel panic或者无限重启。systemd-sysv的作用就是安装/sbin/init这个关键链接。装完配置主机名和时区echo ubuntu-arm /etc/hostname ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime接着安装后续开发调试常用的几个软件包apt install -y openssh-server net-tools ifupdown vim sudo locales \ language-pack-en htop usbutilsopenssh-server就是远程登录用的SSH服务ifupdown提供/etc/network/interfaces的网络配置支持locales和language-pack-en是后面处理中文乱码问题的关键角色。装locales的过程中会提示选择默认语言选en_US.UTF-8就行zh_CN.UTF-8后面再添加。这时还要做一件事设置root用户密码方便开发板启动后用root直接登录passwd root最后给rootfs里创建一个普通用户可选但我建议养成不用root跑日常程序的好习惯useradd -m -s /bin/bash ubuntu echo ubuntu:ubuntu | chpasswd usermod -aG sudo ubuntu4. 网络配置与桌面/Qt运行库选装rootfs的基础系统配置得差不多了这一步要处理的是开发板网络和图形界面程序能不能跑起来的问题。尤其是带屏幕的板子要跑Qt界面运行库一个都不能少。4.1 网络配置DHCP自动获取还是静态IProotfs里apt install net-tools ifupdown之后最好用传统/etc/network/interfaces的方式管理网络因为它在各类ARM开发板上最“通用”。系统启动后如何配置网络取决于你的板子用的什么网卡接口。先修改/etc/network/interfacesecho auto lo /etc/network/interfaces echo iface lo inet loopback /etc/network/interfaces echo auto eth0 /etc/network/interfaces echo iface eth0 inet dhcp /etc/network/interfaces如果你的板子网卡不是eth0有些设备是end0开机后先用ip addr查一下网卡名称再回来改或者更省事的方式是写成auto eth0的同时在/etc/udev/rules.d/里把网卡名称固定下来。这里介绍一个通用的方案在rootfs里添加/etc/systemd/network/20-wired.network[Match] Namee* [Network] DHCPyessystemd-networkd会自动匹配e开头的有线网卡并启用DHCP避免接口命名差异带来的麻烦。相应地要确保systemd-networkd服务被启用了systemctl enable systemd-networkd注意如果你用的是带桌面环境或者板厂自带内核的网络管理方案尽量不要同时开两套网络配置工具比如NetworkManager和ifupdown同时抢网卡会出现IP可以获取但ping不通外网的诡异问题。嵌入式场景网络配置越简单越可靠。4.2 静态IP配置与DNS常见坑有些场景要用静态IP比如板子和上位机直接网线连接调试。这时/etc/network/interfaces改成这样auto eth0 iface eth0 inet static address 192.168.1.100 netmask 255.255.255.0 gateway 192.168.1.1 dns-nameservers 223.5.5.5 114.114.114.114顺手检查一下之前复制的/etc/resolv.conf确保里面有nameserver配置。静态IP最常见的坑是忘配gateway和DNS导致能ping通局域网但访问不了外网排查思路后面单独讲。4.3 Qt运行库安装与交叉编译程序的GLIBC问题跑Qt界面程序的人这时候就要把运行库装上了千万别只交叉编译了个Qt程序拷贝到板子上就指望它运行。Qt程序依赖一堆动态库缺一个就给你error while loading shared libraries。在chroot里直接:apt install -y libqt5widgets5 libqt5gui5 libqt5core5a libgomp1 \ libgl1-mesa-dev libsqlite3-0 libfontconfig1 libfreetype6如果你板子上的Qt程序是Qt 5.5.10这种老版本交叉编译出来的Ubtunu 20.04的Qt库版本是5.12.8直接运行老程序的动态库方式很可能会遇到找不到libQt5Core.so.5的问题。经验做法是用readelf -d 你的程序看一下NEEDED字段再用ldd查看运行环境缺哪些库然后在某个目录里面做软链接把板子上的库路径统一指向系统的库目录ln -s /usr/lib/arm-linux-gnueabihf/libQt5Core.so.5.12.8 /usr/lib/libQt5Core.so.5GLIBC的问题更隐蔽。有时候你在x86编译机上用老交叉编译器比如arm-linux-gnueabihf-gcc 5.x编出来的程序拷到Ubuntu Base 20.04上会报GLIBC_2.29 not found这类错误因为交叉编译器的libstdc/glibc版本比系统的老。反过来在Ubuntu 20.04上装的交叉编译工具链编出的程序放在老rootfs里又会嫌GLIBC_2.31太高。这块没有银弹最好的办法是保持交叉编译工具链与目标rootfs的系统版本对应比如rootfs是20.04就优先用arm-linux-gnueabihf-gcc 9.x这一代工具链。5. 打包镜像与烧录实战记录rootfs配置完成、该装的软件都装齐了接下来就是把它变成可以被开发板启动的镜像。这一步的核心是把rootfs目录复制进一个分好区的SD卡镜像文件里再把镜像dd写进SD卡。听起来不难但坑都在细节里。5.1 制作SD卡镜像文件与分区先退出chroot环境exit将挂载的虚拟文件系统卸载干净sudo umount ~/arm-rootfs/ubuntu-base/dev/pts sudo umount ~/arm-rootfs/ubuntu-base/dev sudo umount ~/arm-rootfs/ubuntu-base/sys sudo umount ~/arm-rootfs/ubuntu-base/proc创建一个合适的镜像文件比如你打算做一张8GB的SD卡dd if/dev/zero ofubuntu-arm.img bs1M count4096然后对镜像文件分区第一分区256MB格式化成FAT放内核和设备树文件剩余空间格式化成ext4放rootfsfdisk ubuntu-arm.imgfdisk里面依次做这些用o新建空的DOS分区表n然后回车选主分区大小输入256Mt把类型改为bFAT32然后n再建第二个分区占满剩余空间w保存退出。分区完让内核重新读取分区表sudo partprobe ubuntu-arm.img sudo kpartx -av ubuntu-arm.imgkpartx会把镜像文件里面的两个分区映射成/dev/mapper/loop0p1和/dev/mapper/loop0p2这样的设备节点这样后续就能直接格式化。格式化命令如下sudo mkfs.vfat -n boot /dev/mapper/loop0p1 sudo mkfs.ext4 -L rootfs /dev/mapper/loop0p2提示如果你的开发板要求SD卡的启动分区是FAT16或者使用特定标识请以板子的BSP文档为准。多数ARM开发板比如NXP i.MX系列、全志系列并不强制文件系统格式uboot只要能读内核到内存就行FAT32覆盖面最广。5.2 复制rootfs与内核文件到镜像把ext4分区挂载到本地目录然后把rootfs整个复制进去mkdir -p /tmp/rootfs-mount sudo mount /dev/mapper/loop0p2 /tmp/rootfs-mount sudo cp -a ~/arm-rootfs/ubuntu-base/. /tmp/rootfs-mount/ synccp的时候务必带上-a参数保留符号链接和权限。我曾经图快用过cp -r结果/bin/sh等一堆符号链接权限丢失系统起不来。复制完成后还有一个极其容易踩的坑rootfs目录里qemu-arm-static是给x86宿主机chroot用的调试工具它不应该出现最终镜像里需手动删除sudo rm /tmp/rootfs-mount/usr/bin/qemu-arm-static再同步一次sync接着挂载FAT分区把开发板对应的zImage/uImage和.dtb设备树文件复制进去mkdir -p /tmp/boot-mount sudo mount /dev/mapper/loop0p1 /tmp/boot-mount sudo cp 你的内核目录/arch/arm/boot/zImage /tmp/boot-mount/ sudo cp 你的内核目录/arch/arm/boot/dts/你的板子.dtb /tmp/boot-mount/ sudo umount /tmp/boot-mount5.3 写卡实测与启动参数调整镜像准备完毕卸载分区映射后写入SD卡sudo umount /tmp/rootfs-mount sudo kpartx -d ubuntu-arm.img sudo dd ifubuntu-arm.img of/dev/sdX bs4M statusprogress sync警告/dev/sdX请务必替换成你的SD卡实际设备号。写卡操作不可逆写错了就把宿主机硬盘全毁了。操作前用lsblk确认容量和挂载点不要凭感觉。把SD卡插到开发板上通过串口连接电脑上电看启动日志。如果uboot找不到rootfs一般会停在类似“VFS: Unable to mount root fs”的提示这时候需要进入uboot命令行修改内核启动参数手动指定root分区setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rootwait bootrootwait参数很重要不加的话内核可能还没等到SD卡完全初始化就去挂载根文件系统导致挂载失败。选mmcblk0p2是因为SD卡是第一个MMC设备第2分区存放rootfs。如果板子从eMMC启动路径要相应改成/dev/mmcblk1p2之类。板子正常情况下应该能进入Ubuntu的登录提示符了用户名root加你之前在chroot里设置的密码就能登录。6. 常见问题排查与避坑速查系统能启动只是第一步实际开发中会发现各种稀奇古怪的问题。下面从自己亲身踩过的坑里挑几个高频的整理成速查清单按现象、原因、解决三步走来说明。6.1 网络相关问题集锦能ping通局域网但ping不通外网。这种情况十有八九是网关或DNS没配对。先ip route看看默认路由是否存在没有的话手动添加再看cat /etc/resolv.conf确保有nameserver条目。有些板子的内核编译时关掉了某些网络协议模块也要排查内核.config里CONFIG_IP_PNP、CONFIG_NETFILTER之类的项。DNS解析总是超时导致apt update失败。首选用nslookup或getent hosts baidu.com测试解析。如果系统里没有nslookup直接修改/etc/resolv.conf改用国内DNS然后systemd-resolve --flush-caches刷新缓存。网络慢的情况可以把apt源改成清华的ubuntu-ports这能大幅提升下载速度。有线网卡名字不是eth0而是enp2s0之类。传统ifupdown配置成了eth0就起不来。要么改interfaces里的名称要么加udev规则固定网卡名。后者方式ip link show先记下实际名称再在/etc/udev/rules.d/70-persistent-net.rules里写固定规则。6.2 中文显示乱码与locale配置很多人在带屏幕的板子上跑Qt或者直接在终端看中文日志看到的是一堆方块或问号这就是典型的locale和字体问题。在Ubuntu Base这种精简rootfs上默认可能连中文字体都没有。在chroot里执行apt install -y fonts-wqy-zenhei fonts-wqy-microhei字体装完再配置好locale支持# 在chroot中 apt install -y locales sed -i s/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/ /etc/locale.gen locale-gen然后在/etc/default/locale里设置LANGen_US.UTF-8 LANGUAGEen_US:en这里有一个坑很多人的程序是中文输出终端却显示乱码遂把LANG改成了zh_CN.UTF-8结果乱得更厉害——因为没有中文字体渲染。所以我的建议是系统级locale保持en_US.UTF-8程序里要显示中文的地方让它明确使用UTF-8编码配合中文字体。乱码问题九成是字体缺失只有一成是locale没配对。6.3 启动阶段报错与库依赖问题执行程序报“Exec format error”。这是因为rootfs架构armhf和程序架构arm64不匹配或者反过来。用file 程序名确认一下架构。还有一种可能rootfs里binfmt_misc配置错误导致内核试图用宿主机的解释器去执行ARM程序这在嵌入式目标板上几乎不会出现更多是开发者把qemu-user编译机制带进了产品环境。启动时卡在init或者反复重启。多数是rootfs没装systemd-sysv导致/sbin/init缺失。chroot进去装一下即可。另一个常见原因是/etc/fstab里写了不存在的分区设备路径内核挂载rootfs成功了但启动流程卡在“Waiting for /dev/xxx to appear”给它注释掉问题就解决。Qt程序提示缺少libGL或EGL库。Ubuntu Base默认不带GPU相关的驱动库但Qt的libQt5Gui.so.5运行时会尝试加载libGL、libEGL。缺什么就装什么apt install -y libgl1-mesa-dri libegl-mesa0 libgles2-mesa如果板子上的GPU有专门BSP驱动就按板厂说明安装对应闭源库。跑软渲染的Qt程序QT_OPENGLsoftware环境变量能帮你绕开大部分图形驱动问题。6.4 根文件系统缩小/扩容与sync异常SD卡启动后如果根分区只占了一开始镜像里设定的4GB容量而实际SD卡有16GB就需要扩容。最简单方式# 在开发板上 fdisk /dev/mmcblk0删除第二个分区重建为剩余全部空间然后resize2fs /dev/mmcblk0p2在线扩容。如果开发板系统里没有fdisk先apt install fdisk。代码或数据库频繁写盘时遇到以下现象power键按下后系统迟迟不关机或重启后部分文件缺失。这就是sync没有完成引起的。SD卡和eMMC的写缓存策略跟PC不太一样很多开发板默认开启了比较激进的写缓存掉电时缓存里的数据全部丢失。解决方式确认应用层每次关键写入都调用了fsync另外在/etc/fstab里给根分区挂载参数加上dataorderedext4默认和commit60减少脏页回写频率/dev/mmcblk0p2 / ext4 defaults,noatime,commit60 0 16.5 交叉编译工具链与rootfs版本匹配用老版本交叉编译器在20.04上编代码时头文件和库路径经常对不上。我建议用以下组合对于ARM 32位目标arm-linux-gnueabihf-gcc-9或者Linaro的gcc-arm-9.x目标系统的GLIBC版本与Ubuntu 20.04基本吻合对于ARM 64位直接用Ubuntu的gcc-aarch64-linux-gnu版本也是9.x编译出来的动态链接程序拷贝到rootfs里能直接跑。如果是拿板厂的旧BSP SDK比如自带arm-linux-gnueabihf-gcc 4.9那些老古董编译出来的程序最好把相应的运行时库一起复制到rootfs的/usr/local/arm-linux-gnueabihf/lib之类目录再通过LD_LIBRARY_PATH指定加载路径这比强行修改系统GLIBC版本稳妥得多。7. 一些实际操作中的体会用Ubuntu Base 20.04做ARM开发板根文件系统这件事前前后后我在好几个不同平台的板子上验证过包括i.MX6ULL、T113、RK3568这类常见芯片。整体感受是一旦rootfs做好并成功启动后面应用的迭代效率比用Buildroot/Yocto高出一个量级毕竟绝大多数开源软件都能在apt源里找到纯应用层开发根本不需要折腾编译环境。如果你准备长期维护这块板子建议把做好的rootfs打包存档同时写一个简单的部署脚本把下载、解压、配置、打包的步骤固化下来。这样以后要出个新版本改几条配置命令重新跑一遍脚本一个干净的rootfs就出来了不用每次手工敲命令从头来过。最后啰嗦一句你的板子内核和设备树是另一个独立的话题rootfs做得再好内核起不来一样白搭。动手做rootfs之前先用官方SDK烧一次系统确认板子能正常启动排除硬件和调试串口问题再开始定制系统。开发板这种东西硬件、uboot、内核、rootfs各自为政的问题最让人头疼把边界理清了出问题了才知道该往哪个环节去查。
返回列表