VMware Ubuntu 20.04 LTS 使用Qemu虚拟机u-boot启动或者配合busybox模拟ARM开发板

这里提供相关工具的下载地址

Linux内核下载地址
busybox下载地址
Qemu下载地址
u-boot下载地址

文中使用的版本为Linux-4.9.268、busybox-1.33.0、qemu-5.2.0、u-boot-2021.01-rc4
版本差别不大,应该都没有问题的

一、busybox制作根目录,通过镜像启动linux内核模拟ARM板

1、编译linux内核

编译条件:gcc编译器,交叉编译器

sudo apt install gcc
sudo apt install gcc-arm-linux-gnueabi

具体安装配置见:交叉编译器安装配置

清除做过的配置

make clean

image
自定义内核配置,通过空格键选择配置。这里不做修改,采用默认配置,后面可以根据需要再修改。

make menuconfig

image

如果运行make menuconfig出错,则需要安装如下的支持包

sudo apt-get install lncurses-dev
sudo apt-get install libncurses5-dev

保存退出后会自动生成.config配置文件

然后创建一个脚本build.sh
输入下面内容,运行即可编译

#! /bin/sh
make CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm vexpress_defconfig
make CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm

image

运行命令,开始编译,内容比较多,会花一些时间

sudo sh build.sh

image

然后会在源码目录下的arch/arm/boot/文件夹下生成镜像Image和zImage

2、编译busybox

进入到busybox目录下,编辑Makefile

搜索如下两个变量,更改成这样

ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabi-

执行下面命令,使得编译器生效

source /etc/profile

修改配置文件

make menuconfig

如果出现没有<curses.h>头文件,尝试安装如下支持包

sudo apt-get install libncurses5-dev

修改配置完成后,make编译

image

再make install生成默认文件_install

image

文件内容

image

记住这个_install文件夹位置,制作根目录的时候会用到

3、编译安装qemu

为了更好的使用qemu,安装前先下载一些安装包

#必要安装包
sudo apt-get install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev

#推荐安装包
sudo apt-get install git-email
sudo apt-get install libaio-dev libbluetooth-dev libbrlapi-dev libbz2-dev
sudo apt-get install libcap-dev libcap-ng-dev libcurl4-gnutls-dev libgtk-3-dev
sudo apt-get install libibverbs-dev libjpeg8-dev libncurses5-dev libnuma-dev
sudo apt-get install librbd-dev librdmacm-dev
sudo apt-get install libsasl2-dev libsdl1.2-dev libseccomp-dev libsnappy-dev libssh2-1-dev
sudo apt-get install libvde-dev libvdeplug-dev libvte-2.90-dev libxen-dev liblzo2-dev
sudo apt-get install valgrind xfslibs-dev

#额外安装包
sudo apt-get install libnfs-dev libiscsi-dev

自带的qemu可能比较老,这里采用安装包的方式安装新版本

解压后进入qemu文件夹

这里选择一种在其他目录安装的方式,比较安全,不会向原目录增加多余的文件

 # 在qemu根目录打开终端
 # 准备一个本机debug版本
 mkdir -p bin/debug/native cd bin/debug/native
 # 配置 QEMU 并启动构建
 ../../../configure --enable-debug

如果提示没有Ninja,则进行下面的操作

#安装支持包
sudo apt install re2c
#安装编译ninja
git clone git://github.com/ninja-build/ninja.git && cd ninja
./configure.py --bootstrap
#如果提示没有python,则sudo apt install python
#如果还报错没有c++,则安装sudo apt-get install g++,装完后再次执行上面的命令
ls   #查看生成的ninja文件
cp ninja /usr/bin/    #拷贝ninja文件到/usr/bin/路径下
#检查ninja版本
ninja --version
#如果检查报错就安装
sudo apt install ninja-build

image

错误解决,再回到出错的步骤继续执行

cd ..   #回到native目录
../../../configure --enable-debug

image

配置完成,然后make编译

这里有8000多行的编译,时间会比较长

image

安装

sudo make install

image

# 返回到QEMU根目录
cd ../../..

运行./configure --help 可以获得帮助信息

测试qemu编译是否正常,输入命令

bin/debug/native/x86_64-softmmu/qemu-system-x86_64 -L pc-bios

image

弹出QEMU界面,安装成功。由于还没有制作根目录,所以显示没有设备

根据界面顶端提示,使用ctrl+alt+g退出输入界面

可以用qemu-system-arm -machine help命令来查看所支持的开发板

image

4、制作启动根目录

自己选择一个地方创建rootfs目录

image

在rootfs里面创建下面的这些空目录(和busybox的_install文件夹里重复的文件夹也可以不用创建,马上要复制过来)

image

(1)完善编译环境

先将刚刚busybox里生成的_install文件夹内部的全部内容复制到rootfs文件夹里。

在rootfs文件夹内打开终端

sudo cp -r ../../busybox-1.33.0/_install/* ./

命令里的文件夹路径根据自己的情况来写

image

当然lib库还是不够的,因为文件系统运行在arm平台,所以还需要arm-linux-gnueabi的库,直接从系统里安装的交叉编译器里复制过来。

sudo cp -p /usr/arm-linux-gnueabi/lib/* ./lib

image

接着来完善根目录文件

(2)完善设备文件dev内容

从终端进入dev文件夹

  • 创建四个串口设备
sudo mknod -m 666 tty1 c 4 1
sudo mknod -m 666 tty2 c 4 2
sudo mknod -m 666 tty3 c 4 3
sudo mknod -m 666 tty4 c 4 4
  • 创建控制台
sudo mknod -m 666 console c 5 1
  • 创建null
sudo mknod -m 666 null c 1 3

image

(3)完善配置文件etc内容

从终端进入etc文件夹
image

  • 创建inittab
::sysinit:/etc/init.d/rcS
#::respawn:-/bin/sh
#::respawn:-/bin/loginconsole
::askfirst:-/bin/sh
#tty2::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
  • 创建fstab
proc    /proc      proc     defaults    0   0
none    /dev/pts   devpts   mode=0622   0   0
mdev    /dev       ramfs    defaults    0   0
sysfs   /sys       sysfs    defaults    0   0
tmpfs   /dev/shm   tmpfs    defaults    0   0
tmpfs   /dev       tmpfs    defaults    0   0
tmpfs   /mnt       tmpfs    defaults    0   0
var     /dev       tmpfs    defaults    0   0
ramfs   /dev       ramfs    defaults    0   0
  • 创建profile
# /etc/profile: system-wide .profile file for the Bourne shells

echo "-----------------------------------"
echo "Mini2440 FileSystem is Ready ..."
echo "-----------------------------------"

USER="`id -un`"
LOGNAME=$USER
#PS1='[\u@\h \W]\# ' #显示主机名、当前路径等信息
PS1='pymeia@Mini2440:\w # ' #显示主机名、当前路径等信息
PATH=$PATH
HOSTNAME=`/bin/hostname`

export USER LOGNAME PS1 PATH
  • 创建init.d/rcS
#! /bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin #可执行程序 环境变量
export LD_LIBRARY_PATH=/lib:/usr/lib #动态链接库 环境变量
/bin/mount -n -t ramfs ramfs /var
/bin/mount -n -t ramfs ramfs /tmp
/bin/mount -n -t sysfs none /sys
/bin/mount -n -t ramfs none /dev
/bin/mkdir /var/tmp
/bin/mkdir /var/modules
/bin/mkdir /var/run
/bin/mkdir /var/log
/bin/mkdir -p /dev/pts //telnet服务需要
/bin/mkdir -p /dev/shm //telnet服务需要
#echo /sbin/mdev > /proc/sys/kernel/hotplug//USB自动挂载需要
/sbin/mdev -s //启动mdev在/dev下自动创建设备文件节点
/bin/mount -a

echo "-----------------------------------"
echo " welcome to Mini2440 board"
echo "-----------------------------------"
  • 创建group
root:*:0:
daemon:*:1:
bin:*:2:
sys:*:3:
adm:*:4:
tty:*:5:
disk:*:6:
lp:*:7:
lpmail:*:8:
news:*:9:
uucp:*:10:
proxy:*:13:
kmem:*:15:
dialout:*:20:
fax:*:21:
voice:*:22:
cdrom:*:24:
floppy:*:25:
tape:*:26:
sudo:*:27:
  • 创建passwd
root::0:0:root:/:/bin/sh
ftp::14:50:FTP User:/var/ftp:
bin:*:1:1:bin:/bin:

创建var文件夹,存放日志信息

sudo ln -s /tmp var/lock
sudo ln -s /tmp var/log
sudo ln -s /tmp var/run
sudo ln -s /tmp var/tmp

5、qemu启动开发板镜像

制作根文件系统镜像,把rootfs根目录的内容复制到这个镜像中,然后用qemu启动模拟开发板。

方法1

制作磁盘镜像启动方式

(1)制作磁盘镜像img

制作磁盘镜像并格式化

qemu-img create -f raw disk.img 512M   #生成512M大小的磁盘镜像
mkfs -t ext4 ./disk.img		#把磁盘镜像格式化成ext4文件系统

也可以将其打包成mkdisk.sh脚本,方便再次创建

sudo sh mkdisk.sh
#!/bin/sh
qemu-img create -f raw disk.img 512M
mkfs -t ext4 ./disk.img

image

(2)复制rootfs内容到磁盘镜像

将rootfs根目录中所有内容复制到磁盘镜像中

mkdir tmpfs #创建一个临时文件夹,文件夹与rootfs同级
sudo mount -o loop ./disk.img tmpfs/  #创建挂载点并挂载
sudo cp -r rootfs/* tmpfs/		#复制文件到镜像
sudo umount tmpfs		#卸载

也可以打包起来方便更新磁盘内容

sudo sh update.sh
#!/bin/sh
sudo mount -o loop ./disk.img tmpfs/
sudo cp -r rootfs/* tmpfs/
sudo umount tmpfs

image

更新期间会将rootfs的内容复制到tmpfs,再删除

image

检查镜像文件信息

file disk.img
(3)启动开发板
不带qemu界面

直接在终端启动,不带lcd界面

sudo sh runnolcd.sh
#! /bin/sh
qemu-system-arm -M vexpress-a9 -m 512M -dtb ../linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -kernel ../linux-4.9.268/arch/arm/boot/zImage -nographic -append "root=/dev/mmcblk0 rw console=ttyAMA0" -sd disk.img

如果遇到拒绝访问的情况,请授予文件访问权限,删掉磁盘重新创建(直接更新不成功)

image

chmod +x rcS
chmod -R 777 init.d/*
或者授予etc的所有文件访问权限
chmod -R 777 etc/

启动界面如下

image

带qemu界面

带lcd界面启动方式

sudo sh runlcd.sh
#! /bin/sh
qemu-system-arm -M vexpress-a9 -m 512M -dtb ../linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -kernel ../linux-4.9.268/arch/arm/boot/zImage -append "root=/dev/mmcblk0 rw console=tty0" -sd disk.img

启动界面如下

image

【注意】:启动终端的命令,里面的路径一定要根据自己创建的文件夹填写正确。启动命令也可以直接在终端输入,但是比较麻烦。

方法2

制作sd根文件系统镜像启动方式

(1)制作sd卡镜像

生成虚拟sd卡并格式化为ext格式

sudo sh mksd.sh
#! /bin/sh
dd if=/dev/zero of=rootfs.ext3 bs=1M count=32
mkfs.ext3 rootfs.ext3
(2)复制内容到sd镜像

将虚拟sd卡挂载到/mnt,拷贝rootfs的所有文件到sd,然后卸载sd

sudo sh update.sh
#! /bin/sh
mount -t ext3 rootfs.ext3 /mnt/ -o loop
cp -r rootfs/* /mnt
umount /mnt

(3)启动开发板

不带qemu界面
sudo sh runnolcd.sh
#! /bin/sh
qemu-system-arm -M vexpress-a9 -m 512M -dtb ../linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -kernel ../linux-4.9.268/arch/arm/boot/zImage -nographic -append "root=/dev/mmcblk0 rw console=ttyAMA0" -sd rootfs.ext3
带qemu界面
sudo sh runlcd.sh
#! /bin/shqemu-system-arm -M vexpress-a9 -m 512M -dtb ../linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -kernel ../linux-4.9.268/arch/arm/boot/zImage -append "root=/dev/mmcblk0 rw console=tty0" -sd rootfs.ext3

6、关闭虚拟开发板

方法一:直接关掉终端

方法二:ctrl +a 放手,然后按x

方法三:打开另一个终端输入

sudo killall qemu-system-arm

二、u-boot启动Linux内核方式,模拟ARM开发板

1、生成uImage内核映像

我们的目的是:uImage文件

生成它的步骤是:编译u-boot生成mkimage—>用mkimage生成uImage

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这里解释一下uImage:

Linux内核编译(make)之后会生成两个文件,一个Image,一个zImage,其中Image为内核映像文件,而zImage为内核的一种映像压缩文件,Image大约为4M,而zImage不到2M

而uImage是用mkimage工具根据zImage制作而来的,它是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(1)编译u-boot
修改配置

进入u-boot源码目录,分别进入Makefile和config.mk修改配置

修改顶层Makefile,指定交叉编译器

CROSS_COMPILE ?= arm-linux-gnueabi-

修改顶层config.mk,指定ARM架构

ARCH := arm
编译
#配置开发板
make vexpress_ca9x4_defconfig
#编译u-boot
make –j4

image

如果编译出错,缺少如下配置

image

则安装如下,解决问题

sudo apt install bison flex

如果make -j4报错可能是上面的配置没有起到作用,则尝试如下命令

export CROSS_COMPILE=arm-linux-gnueabi-
export ARCH=arm
make clean
make vexpress_ca9x4_defconfig
make -j4

编译完成后如图

image

测试效果

在u-boot源码路径下,运行u-boot看是否成功

qemu-system-arm -M vexpress-a9 -kernel u-boot -nographic -m 512M

这里由于u-boot还没有uImage镜像,让倒计时结束自动加载会出问题。看到提示后,快速按任意按键,停止自动加载,出现下图,说明编译成功。

image

(2)拷贝mkimage

编译完u-boot源码,会在tools目录下生成mkimage,用它可以将zImage转变成uImage

把u-boot目录下tools/mkimage 拷贝到/usr/bin目录下,为了可以执行mkimage相关的命令。

如果不拷贝直接执行命令,你会发现/usr/bin缺少文件

image

除此之外,还可以,直接命令下载,也会再该/usr/bin目录下生成mkimage

sudo apt install u-boot-tools

这种方法下载自带的u-boot-tools不知道会不会出问题,可以尝试尝试。

为nfs服务做准备

再生成uImage前,需要nfs配置的修改,重新编译Linux内核,让内核支持nfs功能,生成新的zImage,然后再转化成uImage

修改配置如下,在Linux内核目录下输入:make menuconfig

进入File system —>

image

再进入Network File System —>

image

将带有NFS的都用空格键选上

image

选中后如图

image

保存退出

(3)生成uImage(疑问待解决)

方法一:

直接按照以下命令重新编译(待验证,地址也有问题)

 export ARCH=arm
 export CROSS_COMPILE=arm-linux-gnueabi-
 make vexpress_defconfig
 make zImage -j8
 make modules -j8
 make LOADADDR=0x60003000 uImage -j8
 make dtbs

方法二:将编译生成的zImage转化成uImage(也是地址问题待解决)

切换到Linux内核目录下,执行下面命令

mkimage -n 'mini2440' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -d  arch/arm/boot/zImage  arch/arm/boot/uImage

参数说明

-n 'mini2440'   #指定镜像名称
-A arm          #设置为arm架构
-O linux        #设置操作系统为linux
-T kernel       #设置镜像类型为kernel
-C none         #设置压缩类型为none
-a 0x30008000   #指定加载地址为0x30008000
-e 0x30008040   #执行入口地址为0x30008040
-d arch/arm/boot/zImage #指定镜像数据文件路径
arch/arm/boot/uImage    #最终生成的uImage的路径和文件名称

这里直接在zImage旁边生成uImage

image

image

到此uImage镜像生成结束

然后将uImage文件拷贝到tftpboot目录下(这一步也可以后面再一起做),这个文件夹是在配置tftp的时候创建的。

sudo cp ../../linux-4.9.268/arch/arm/boot/uImage ./		#路径根据自己的情况来

image

2、安装配置nfs服务

在前面我们已经装好了tftp服务器,但还需要安装nfs服务

(1)安装
sudo apt-get install  nfs-kernel-server

image

(2)创建nfs共享目录,设置权限
sudo chmod 777 ./rootfs

image

(3)添加NFS共享目录路径
vi /etc/exports
在/etc/exports添加以下路径
/home/pymeia/qemu/ARM/ubootA9/rootfs *(rw,sync,no_subtree_check)
其中:
	rw    可读可写操作
	sync    内存和磁盘上的内容保持同步

	no_root_squash    Linux主机不再将开发板设置为匿名用户,可以操作文件读写
	注意:Ubuntu20.04,这个文件里不填这个参数,会报错。exports文件里有参考实例说明,只有三个参数

	no_subtree_check    不检查根文件系统子目录文件
(4)重启nfs服务
sudo /etc/init.d/rpcbind restart
sudo /etc/init.d/nfs-kernel-server restart
或者
systemctl restart nfs-kernel-server
(5)检查nfs是否启动正常
sudo showmount -e
#显示全部可以挂载的目录
sudo /etc/init.d/nfs-kernel-server status	#查看nfs服务的当前状态
ps -e |grep nfs		#看进程中nfs服务是否启动

image

(6)nfs使用

自己创建一个目录当作nfs目录,然后再/etc/exports中添加路径,可以把你的程序放到这个目录下,通过挂载映射到./mnt。./mnt可以再主机上,也可以在开发板上,方便进行代码的调试。

在本机localhost上挂载nfs目录到./mnt,这样rootfs的内容就映射到了./mnt

image

sudo mount -t nfs localhost:/home/pymeia/qemu/ARM/ubootA9/rootfs ./mnt/   
#这里挂载的目录一定要在/etc/exports里添加,否则挂载无效

image

卸载后,文件删除

umount ./mnt

同理,在开发板上挂载nfs目录到/mnt(假设主机IP地址为192.168.146.129)

mount -t nfs -o nolock 192.168.146.129:/dev/nfs  /mnt	
#其中:-o nolock是去除文件锁,否则会报错

3、安装配置tftp服务

(1)安装tftp
sudo apt-get install tftp-hpa tftpd-hpa xinetd
(2)更改配置文件
sudo cp  /etc/default/tftpd-hpa  /etc/default/tftpd-hpa_back	#备份原始设置
sudo gedit /etc/default/tftpd-hpa

填写自己要创建tftpboot文件的路径

image

(3)创建tftp目录,设置权限

配置好后,到上面填写的路径,创建文件夹,并设置权限

#切换到上面填写的路径
sudo mkdir tftpboot
sudo chmod 777 tftpboot
(4)重启tftp服务,让其生效
sudo /etc/init.d/tftpd-hpa restart

image

(5)测试tftp服务是否正常

把需要的文件如u-boot、dtb、uImage等拷贝到tftpboot文件夹(后面u-boot启动要用到)

uImage

cp -r /home/pymeia/qemu/ARM/linux-4.9.268/arch/arm/boot/uImage ./

u-boot

cp -r /home/pymeia/qemu/ARM/ubootA9/u-boot/u-boot ./

vexpress-v2p-ca9.dtb

cp -r /home/pymeia/qemu/ARM/linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb ./

整理copy.sh

#! /bin/sh
cp -r /home/pymeia/qemu/ARM/linux-4.9.268/arch/arm/boot/uImage ./
cp -r /home/pymeia/qemu/ARM/ubootA9/u-boot/u-boot ./
cp -r /home/pymeia/qemu/ARM/linux-4.9.268/arch/arm/boot/dts/vexpress-v2p-ca9.dtb ./

image

tftp 192.168.146.129   #主机ip可以用ifconfig来查看
tftp> get uImage	#获取文件,保存到用户名目录下
tftp> q	#退出tftp
ps -e | grep "tftp"	#查看 tftp  的进程号

image

可以看到,获取的文件和获取失败的文件都会显示在用户目录下

image

4、配置qemu和ubuntu网络桥接功能(这一部分,暂时没有搞完)

VMware 需要配置成NAT模式, 在VMware的桥接模式下面,目前qemu无法成功连接网络
VMware也不能设置成静态ip地址,设置成静态ip地址也是无法连接网络

安装桥接依赖包

sudo apt install uml-utilities bridge-utils
#bridge-utils    虚拟网桥工具
#uml-utilities   UML(User-mode linux)工具

查看是否tun设备文件,如果存在说明宿主机的内核支持TAP网络接口

ls /dev/net

image

如果没有,则要手动创建

sudo mkdir /dev/net
sudo mknod /dev/net/tun c 10 200

加载tun模块

sudo /sbin/modprobe tun

非常建议更改重要配置的节点时,及时创建虚拟机快照,方便恢复,已经重装一次,吸取教训了

下面是大雷区,不要乱用,或者在用之前创建还原快照,否则会直接导致Ubuntu 20.04 LTS失去网络,甚至运气差点要重装

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

修改配置文件(重启生效)

sudo gedit /etc/network/interfaces

添加以下内容

# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopback

auto ens33
auto br0
iface br0 inet dhcp
bridge_ports ens33

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

添加qemu有关系统脚本

在**/etc/qemu-ifup**文件中添加以下内容

#!/bin/sh

echo sudo tunctl -u $(id -un) -t $1
sudo tunctl -u $(id -un) -t $1
echo sudo ifconfig $1 0.0.0.0 promisc up
sudo ifconfig $1 0.0.0.0 promisc up
echo sudo brctl addif br0 $1
sudo brctl addif br0 $1
echo brctl show
brctl show

sudo ifconfig br0 192.168.146.129
# 根据自己的实际情况修改 IP地址,注意:uboot 中的 CONFIG_SERVERIP(serverip) 要跟这里一样

在**/etc/qemu-ifdown**文件中添加以下内容

#!/bin/sh
echo sudo brctl delif br0 $1
sudo brctl delif br0 $1
echo sudo tunctl -d $1
sudo tunctl -d $1
echo brctl show
brctl show

给上面的脚本添加执行权限

sudo chmod +x /etc/qemu*

重启网络使生效

sudo service network-manager restart

u-boot启动

修改u-boot配置文件include/configs/vexpress_common.h

sudo gedit include/configs/vexpress_common.h
#define CONFIG_BOOTCOMMAND \
		"tftp 0x60003000 uImage;tftp 0x60500000 vexpress-v2p-ca9.dtb; \
		setenv bootargs 'root=/dev/mmcblk0 console=tty0'; \
		bootm 0x60003000 - 0x60500000;"


#define CONFIG_IPADDR 10.0.2.14
#define CONFIG_NETMASK 255.255.255.0
#define CONFIG_SERVERIP 192.168.146.129

所有配置完成后,开始建立u-boot启动内核文件

新建启动脚本ubootqemu.sh

#! /bin/sh
qemu-system-arm \
	-M vexpress-a9 \
	-kernel u-boot  \
	-nographic  \
	-m 512M  \
	-net nic,vlan=0 -net tap,vlan=0,ifname=tap0 \
	-sd rootfs.ext3

未完…问题解决后更新

参考文章链接:
https://blog.csdn.net/u010344264/article/details/82949143
https://zhuanlan.zhihu.com/p/340362172
https://www.jianshu.com/p/8619a6739040
https://www.cnblogs.com/schips/p/12350122.html
https://blog.csdn.net/wxh0000mm/article/details/90056912

Logo

更多推荐