Bootm启动流程分析

  • 如何引导内核
    • uboot启动命令
    • 内核镜像介绍
    • 内核启动前提
  • Bootm命令详解
    • Bootm命令格式
    • do_bootm
      • do_bootm_subcommand
      • images全局变量
    • do_bootmd_states
      • bootm_start
      • bootm_find_os
        • boot_get_kernel
      • bootm_find_other
      • bootm_load_os
      • boot_ramdisk_high
      • boot_relocate_fdt
      • bootm_os_get_boot_func
      • do_bootm_linux
        • boot_prep_linux
        • params全局变量
        • boot_jump_linux
      • 当前images中的值
  • 优化启动时间

如何引导内核

uboot启动命令

  • 从Flash/TFTP加载uImage(zImage、Image),根文件系统、设备树dtb到SDRAM
mmc read image_addr part_off part_size
mmc read ramdisk_addr part_off part_size
mmc read dtb_addr part_off part_size
tftp uImage image_addr
tftp ramdisk ramdisk_addr
tftp dtb dtb_addr
  • 执行bootm(bootz、booti)命令,由内核接管控制权
#bootm/bootz/booti [image_addr] [ramdisk_addr] [dtb_addr]
#任何一项都可以没有,如果没有,用 “-’代替
用法1:bootm                                #使用默认的镜像地址启动
用法2:bootm image_addr                     #指定镜像地址启动。一般是uImage
用法3:bootm image_addr - dtb_addr          #指定设备树地址

内核镜像介绍

# 编译内核时的链接过程
...LD      vmlinuxSORTEX  vmlinuxSYSMAP  System.mapOBJCOPY arch/arm/boot/ImageKernel: arch/arm/boot/Image is readySHIPPED arch/arm/boot/compressed/hyp-stub.SSHIPPED arch/arm/boot/compressed/fdt_rw.cSHIPPED arch/arm/boot/compressed/fdt.hSHIPPED arch/arm/boot/compressed/libfdt.hSHIPPED arch/arm/boot/compressed/libfdt_internal.hSHIPPED arch/arm/boot/compressed/fdt_ro.cSHIPPED arch/arm/boot/compressed/fdt_wip.cSHIPPED arch/arm/boot/compressed/fdt.cSHIPPED arch/arm/boot/compressed/lib1funcs.SSHIPPED arch/arm/boot/compressed/ashldi3.SSHIPPED arch/arm/boot/compressed/bswapsdi2.SLDS     arch/arm/boot/compressed/vmlinux.ldsAS      arch/arm/boot/compressed/head.oGZIP    arch/arm/boot/compressed/piggy_dataCC      arch/arm/boot/compressed/misc.oCC      arch/arm/boot/compressed/decompress.oCC      arch/arm/boot/compressed/string.oAS      arch/arm/boot/compressed/hyp-stub.oCC      arch/arm/boot/compressed/fdt_rw.oCC      arch/arm/boot/compressed/fdt_ro.oCC      arch/arm/boot/compressed/fdt_wip.oCC      arch/arm/boot/compressed/fdt.oCC      arch/arm/boot/compressed/atags_to_fdt.oAS      arch/arm/boot/compressed/lib1funcs.oAS      arch/arm/boot/compressed/ashldi3.oAS      arch/arm/boot/compressed/bswapsdi2.oAS      arch/arm/boot/compressed/piggy.oLD      arch/arm/boot/compressed/vmlinuxOBJCOPY arch/arm/boot/zImageKernel: arch/arm/boot/zImage is ready
# 第一步生成未经过压缩的vmlinux、Image
## vmlinux ---OBJCOPY---> arch/arm/boot/Image
# 第二步生成压缩的vmlinux、zImage
## arch/arm/boot/compressed/vmlinux ---OBJCOPY ---> arch/arm/boot/zImage

vmlinux:Linux内核编译生成原始内核文件,ELF格式,该映像可用于定位内核问题(readelf -s vmlinux),但不能直接引导Linux系统启动。

Image :使用objcopy处理vmlinux后( 去除其中的符号和重定位信息等 )生成的二进制内核映像。该映像未压缩,可直接引导Linux系统启动。

zImage:普通的压缩内核映像文件,使用gzip压缩Image后生成的Linux内核映像。

uImage:使用工具mkimage对普通的压缩内核映像文件(zImage)加工而得。它是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的头,说明这个内核的版本、加载位置、生成时间、大小等信息。

# path:u-boot-2019.04/tool/mkimage
#      linux-4.19.91/scripts/mkuboot.sh
# 帮助文档 u-boot-2019.04/docmkimage.1
Usage: ./mkimage -l image-l ==> list image header information./mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image-A ==> set architecture to 'arch'      #指定CPU的体系结构 arm x86 -O ==> set operating system to 'os'      #指定操作系统类型 linux openbsd-T ==> set image type to 'type'           #指定映象类型 kernel、ramdisk-C ==> set compression type 'comp'     #指定映象压缩方式 none gzip -a ==> set load address to 'addr' (hex)  #指定映象在内存中的加载地址-e ==> set entry point to 'ep' (hex)       #指定映象运行的入口点地址,addr+0x40(头部长度)-n ==> set image name to 'name'           #指定映象名-d ==> use image data from 'datafile'      #指定制作映象的源文件-x ==> set XIP (execute in place)workspace@: ./mkuboot.sh -A arm -O linux -T kernel -C none -a 0x8000 -e 0x8000 -n 'my_kernel' -d ../arch/arm/boot/zImage uImage
Image Name:   my_kernel
Created:      Mon Mar 14 14:46:14 2022
Image Type:   ARM Linux Kernel Image (uncompressed)
Data Size:    1882944 Bytes = 1838.81 kB = 1.80 MB
Load Address: 00008000
Entry Point:  00008000

image header
mkuboot.sh -l uImage

hexdump -C uImage | less

// path:u-boot-2019.04/include/image.h
/** Legacy format image header,* all data in network byte order (aka natural aka bigendian).*/
typedef struct image_header {__be32     ih_magic;   /* Image Header Magic Number(镜像头部幻数为 #define IH_MAGIC    0x27051956)  */__be32        ih_hcrc;    /* Image Header CRC Checksum(镜像头部CRC校验码)  */  __be32      ih_time;    /* Image Creation Timestamp(镜像创建时间戳)  */__be32        ih_size;    /* Image Data Size(镜像数据大小(不算头部))1882944 Bytes       */__be32        ih_load;    /* Data  Load  Address(镜像数据将要载入的内存地址)0x8000       */__be32        ih_ep;      /* Entry Point Address(镜像入口地址)0x8000      */__be32        ih_dcrc;    /* Image Data CRC Checksum(镜像数据CRC校验码)    */      uint8_t     ih_os;      /* Operating System(操作系统类型)05 IH_OS_LINUX     */uint8_t       ih_arch;    /* CPU architecture(CPU架构)02 IH_ARCH_ARM      */uint8_t       ih_type;    /* Image Type(镜像类型)02 IH_TYPE_KERNEL          */uint8_t       ih_comp;    /* Compression Type(压缩类型)00 IH_COMP_NONE      */uint8_t       ih_name[IH_NMLEN];  /* Image Name(镜像名字ih_name,共32字节 #define IH_NMLEN 32 'my_kernel')  */
} image_header_t;

内核启动前提

参考linux-4.19.148/Documentation/arm/Booting和linux-4.19.148/Documentation/arm64/booting.txt
arm架构处理器对linux内核启动之前环境的需求

  1. cpu 寄存器设置
    ARM:
    R0 = 0
    R1 = 板级 id
    R2 = 启动参数在内存中的起始地址
    ARM64:
    x0 = dtb在系统内存的物理地址
    x1 = 0 保留给以后使用
    x2 = 0 保留给以后使用
    x3 = 0 保留给以后使用

  2. cpu 模式
    禁止所有中断
    ARM:必须为SVC(超级用户)模式
    ARM64:处于EL2或者非安全模式的EL1模式中

  3. 缓存、MMU
    关闭 MMU
    指令缓存可以开启或者关闭
    数据缓存必须关闭并且不能包含任何脏数据

  4. DMA 设备应当停止工作

  5. bootloader需要跳转到内核镜像的第一条指令处

  6. ARM64下系统寄存器设置
    内核镜像将要进入的异常级别(EL)前,必须在其更高的异常级别(EL)进行初始化,以防止在一个未知的状态执行。

Bootm命令详解

bootm要做的事情

a. 读取头部,把内核拷贝到合适的地方(bootm_headers_t images)

b. 将启动参数给内核准备好,并告诉内核参数的首地址

c. 设置cpu寄存器,禁止中断,关闭MMU和cache

d. 跳转到内核的入口地址,kernel开始运行

Bootm命令格式

#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, _usage,   \_help, _comp)              \{ #_name, _maxargs, _rep, 0 ? _cmd : NULL, _usage, \_CMD_HELP(_help) _CMD_COMPLETE(_comp) }
//name:命令名,非字符串,但在U_BOOT_CMD中用“#”符号转化为字符串
//maxargs:命令的最大参数个数
//repeatable:是否自动重复(按Enter键是否会重复执行)
//command:该命令对应的响应函数指针
//usage:简短的使用说明(字符串)
//help:较详细的使用说明(字符串)U_BOOT_CMD(bootm,  CONFIG_SYS_MAXARGS, 1,  do_bootm,"boot application image from memory", bootm_help_text
);
Usage:
bootm [addr [arg ...]]- boot application image stored in memorypassing arguments 'arg ...'; when booting a Linux kernel,'arg' can be the address of an initrd imageWhen booting a Linux kernel which requires a flat device-treea third argument is required which is the address of thedevice-tree blob. To boot that kernel without an initrd image,use a '-' for the second argument. If you do not pass a thirda bd_info struct will be passed insteadSub-commands to do part of the bootm sequence.  The sub-commands must be
issued in the order below (it's ok to not issue all sub-commands):start [addr [arg ...]]loados  - load OS imageramdisk - relocate initrd, set env initrd_start/initrd_endfdt     - relocate flat device treecmdline - OS specific command line processing/setupbdt     - OS specific bd_t processingprep    - OS specific prep before relocation or gogo      - start OS#example
bootm                                       # 使用默认的镜像地址启动
bootm image_addr                            # 指定镜像地址
bootm image_addr - dtb_addr                 # 同时指定设备树dtb地址
bootm image_addr ramdisk_addr dtb_addr      # 指定ramdisk、dtb地址
bootm start [image_addr] [ramdisk_addr] [dtb_addr]   # 找到镜像
bootm loados                                # 加载镜像
bootm ramdisk                               # 重新定位initrd,设置环境变量initrd_start/initrd_end
bootm fdt                                   # 重新定位设备树
bootm prep                                  # 跳转内核前的处理,设置启动参数TAGS
bootm go                                    # 跳转到内核入口地址

do_bootm

int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{/* determine if we have a sub command */argc--; argv++;if (argc > 0) {char *endp;simple_strtoul(argv[0], &endp, 16);if ((*endp != 0) && (*endp != ':') && (*endp != '#'))/* 执行子命令 */return do_bootm_subcommand(cmdtp, flag, argc, argv);}/* 执行bootm命令的指定状态 */return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |BOOTM_STATE_LOADOS |
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGHBOOTM_STATE_RAMDISK |
#endifBOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |BOOTM_STATE_OS_GO, &images, 1);
}

do_bootm_subcommand

static cmd_tbl_t cmd_bootm_sub[] = {U_BOOT_CMD_MKENT(start, 0, 1, (void *)BOOTM_STATE_START, "", ""),U_BOOT_CMD_MKENT(loados, 0, 1, (void *)BOOTM_STATE_LOADOS, "", ""),
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGHU_BOOT_CMD_MKENT(ramdisk, 0, 1, (void *)BOOTM_STATE_RAMDISK, "", ""),
#endif
#ifdef CONFIG_OF_LIBFDTU_BOOT_CMD_MKENT(fdt, 0, 1, (void *)BOOTM_STATE_FDT, "", ""),
#endifU_BOOT_CMD_MKENT(cmdline, 0, 1, (void *)BOOTM_STATE_OS_CMDLINE, "", ""),U_BOOT_CMD_MKENT(bdt, 0, 1, (void *)BOOTM_STATE_OS_BD_T, "", ""),U_BOOT_CMD_MKENT(prep, 0, 1, (void *)BOOTM_STATE_OS_PREP, "", ""),U_BOOT_CMD_MKENT(fake, 0, 1, (void *)BOOTM_STATE_OS_FAKE_GO, "", ""),U_BOOT_CMD_MKENT(go, 0, 1, (void *)BOOTM_STATE_OS_GO, "", ""),
};static int do_bootm_subcommand(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[])
{int ret = 0;long state;cmd_tbl_t *c;c = find_cmd_tbl(argv[0], &cmd_bootm_sub[0], ARRAY_SIZE(cmd_bootm_sub));argc--; argv++;if (c) {state = (long)c->cmd;if (state == BOOTM_STATE_START)state |= BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER;} else {/* Unrecognized command */return CMD_RET_USAGE;}if (((state & BOOTM_STATE_START) != BOOTM_STATE_START) &&images.state >= state) {printf("Trying to execute a command out of order\n");return CMD_RET_USAGE;}/* 同样执行bootm命令的指定状态 */ret = do_bootm_states(cmdtp, flag, argc, argv, state, &images, 0);return ret;
}

无论是subcommand还是一般bootm命令,最终都是执行do_bootmd_states函数,区别入参state不同

命令 state
bootm [image_addr] [ramdisk_addr] [dtb_addr] BOOTM_STATE_START | BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER | BOOTM_STATE_LOADOS | BOOTM_STATE_RAMDISK | BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO
bootm start BOOTM_STATE_START | BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER
bootm loados BOOTM_STATE_LOADOS
bootm ramdisk BOOTM_STATE_RAMDISK
bootm fdt BOOTM_STATE_FDT
bootm prep BOOTM_STATE_OS_PREP
bootm go BOOTM_STATE_OS_GO

images全局变量

do_bootmd_states函数的其中一个参数是全局images,是引导内核的一个重要变量。

/* u-boot-2019.04/common/bootm.c */
bootm_headers_t images;     /* pointers to os/initrd/fdt images *//* u-boot-2019.04/include/image.h */
/** Legacy and FIT format headers used by do_bootm() and do_bootm_<os>()* routines.*/
typedef struct bootm_headers {/** Legacy os image header, if it is a multi component image* then boot_get_ramdisk() and get_fdt() will attempt to get* data from second and third component accordingly.*/image_header_t    *legacy_hdr_os;     /* image header pointer 镜像头指针*/image_header_t   legacy_hdr_os_copy; /* header copy 镜像头部*/ulong      legacy_hdr_valid;       /* header是否有效 */#ifndef USE_HOSTCCimage_info_t  os;     /* os image info 镜像信息*/typedef struct image_info {ulong     start, end;     /* start/end of blob blob起始地址和结束地址*/ulong       image_start, image_len; /* start of image within blob, len of image 镜像开始地址和长度*/ulong        load;           /* load addr for the image image的加载地址*/uint8_t      comp, type, os;     /* compression, type of image, os type 压缩类型、镜像类型和操作系统类型*/uint8_t        arch;           /* CPU architecture 架构类型*/} image_info_t;ulong      ep;     /* entry point of OS 入口地址*/ulong        rd_start, rd_end;/* ramdisk start/end ramdisk的起始和结束地址*/char     *ft_addr;   /* flat dev tree address 设备树地址*/ulong       ft_len;     /* length of flat device tree 设备树长度*/ulong      initrd_start;   /* initrd内存根文件系统起始地址 */ulong        initrd_end;     /* initrd内存根文件系统结束地址 */ulong        cmdline_start;  /* cmdline命令行起始地址 */ulong       cmdline_end;    /* cmdline命令行结束地址 */bd_t        *kbd;
#endifint       verify;     /* getenv("verify")[0] != 'n' 是否要对image进行验证*/#ifdef CONFIG_LMBstruct lmb   lmb;        /* for memory mgmt 用来进行内存管理*/
#endif
} bootm_headers_t;

do_bootmd_states

/** @param cmdtp        Pointer to bootm command table entry* @param flag      Command flags (CMD_FLAG_...)* @param argc      Number of subcommand arguments (0 = no arguments)* @param argv        Arguments* @param states   Mask containing states to run (BOOTM_STATE_...)* @param images Image header information* @param boot_progress 1 to show boot progress, 0 to not do this*/
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],int states, bootm_headers_t *images, int boot_progress)
/* 初始化image全局变量,设置bootm的内存 */
|--if (states & BOOTM_STATE_START)
| |--ret = bootm_start
/* find os 确定image的内存地址 */
|--if (!ret && (states & BOOTM_STATE_FINDOS))
| |--ret = bootm_find_os
/* find ramdisk、fdt 确定ramdisk、fdt的内存地址 */
|--if (!ret && (states & BOOTM_STATE_FINDOTHER))
| |--ret = bootm_find_other(cmdtp, flag, argc, argv);
/* Load the OS 加载image */
|--if (!ret && (states & BOOTM_STATE_LOADOS))
| |--ret = bootm_load_os
/* Relocate the ramdisk 重定位ramdisk */
|--if (!ret && (states & BOOTM_STATE_RAMDISK))
| |--ret = boot_ramdisk_high
/* Relocate the fdt 为设备树中的memory reserve预留内存,重定位fdt */
|--if (!ret && (states & BOOTM_STATE_FDT))
| |--boot_fdt_add_mem_rsv_regions
| |--ret = boot_relocate_fdt
/* From now on, we need the OS boot function */
/* get boot_fn 获取启动函数 */
|--boot_fn = bootm_os_get_boot_func(images->os.os)
/* boot_fn-BOOTM_STATE_OS_PREP 设置dtb或者atags */
|--if (!ret && (states & BOOTM_STATE_OS_PREP))
| |--ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
/* boot_fn-BOOTM_STATE_OS_GO 禁止中断,关闭cache,跳转到内核入口地址*/
|--if (!ret && (states & BOOTM_STATE_OS_GO))
| |--ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,images, boot_fn);
| | |--boot_fn(state, argc, argv, images);

bootm_start

  • 初始化images全局变量

  • 根据环境变量verify设置images.verify = 0

  • 根据环境变量bootm_low和bootm_size设置images.lmb

    • setenv bootm_low = 0x0;

    • setenv bootm_size = 0x02000000

    images.lmb.memory.region[0].base = 0x0

    images.lmb.memory.region[0].size = 0x02000000

    images.lmb.reserved.region[0].base = 0x0

    images.lmb.reserved.region[0].size = 0x0

/* 若需要重定位fdt和ramdisk,则从该内存中进行申请 */
lmb->memory.region[0].base = bootm_low;
lmb->memory.region[0].size = bootm_size
lmb->memory.cnt = 1;
lmb->memory.size = 0;/* nt98566上未设置reserved.region */
lmb->reserved.region[0].base = sp - 4096;
lmb->reserved.region[0].size = gd->bd->bi_dram[0].start + gd->bd->bi_dram[0].size - base;
lmb->reserved.cnt = 1;
lmb->reserved.size = 0;
static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[])
|--memset((void *)&images, 0, sizeof(images));
|--images.verify = getenv_yesno("verify");
/* 若定义了CONFIG_LMB,设置images.lmb,用于内存管理,否则为空函数 */
|--boot_start_lmb(&images);
|--images.state = BOOTM_STATE_START;

bootm_find_os

bootm 0x5800000 - 0x5c00000

  • 设置mages.os.image_start = 0x5800000 + 0x40 = 0x5800040
  • 设置images.os.image_len = 0x207c28
  • 将0x5800000上的0x40大小的头部拷贝给images->legacy_hdr_os_copy
  • 设置images->legacy_hdr_os = 0x5800000
  • 设置images->legacy_hdr_valid = 1;
  • 设置images.os.type = image_get_type(os_hdr) = IH_TYPE_KERNEL
  • 设置images.os.comp = image_get_comp(os_hdr) = IH_COMP_NONE
  • 设置images.os.os = image_get_os(os_hdr) = IH_OS_LINUX
  • 设置images.os.end = image_get_image_end(os_hdr) = 0x5800000 + 0x207c28+ 0x40 = 0x5a07c68
  • 设置images.os.load = image_get_load(os_hdr) = 0x8000
  • 设置images.os.arch = image_get_arch(os_hdr) = IH_ARCH_ARM
  • 设置images.ep = image_get_ep(&images.legacy_hdr_os_copy) = 0x8000
  • 设置images.os.start = 0x5800000
static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[])
|--const void *os_hdr;
/* get kernel image header, start address and length */
|--os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,&images, &images.os.image_start, &images.os.image_len);
/* get image parameters */
|--switch (genimg_get_format(os_hdr)) {| |--case IMAGE_FORMAT_LEGACY:
| | |--images.os.type = image_get_type(os_hdr);
| | |--images.os.comp = image_get_comp(os_hdr);
| | |--images.os.os = image_get_os(os_hdr);
| | |--images.os.end = image_get_image_end(os_hdr);
| | |--images.os.load = image_get_load(os_hdr);
| | |--images.os.arch = image_get_arch(os_hdr);
|--}
|--if (images.legacy_hdr_valid)
| |--images.ep = image_get_ep(&images.legacy_hdr_os_copy);
|--images.os.start = map_to_sysmem(os_hdr);
boot_get_kernel
static const void *boot_get_kernel(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[], bootm_headers_t *images,ulong *os_data, ulong *os_len)
|--image_header_t   *hdr;
/* img_addr = simple_strtoul(argv[0], NULL, 16) */
|--ulong img_addr = genimg_get_kernel_addr_fit(argc < 1 ? NULL : argv[0],&fit_uname_config,&fit_uname_kernel);
|--const void *buf = map_sysmem(img_addr, 0);
|--switch (genimg_get_format(buf)){| |--case IMAGE_FORMAT_LEGACY:
/* verify legacy format kernel image */
| | |--hdr = image_get_kernel(img_addr, images->verify);
/* get os_data and os_len */
| | |--switch (image_get_type(hdr)) {| | | |--case IH_TYPE_KERNEL:case IH_TYPE_KERNEL_NOLOAD:*os_data = image_get_data(hdr);*os_len = image_get_data_size(hdr);
| | |--}
/* copy image header to allow for image overwrites during kernel decompression.*/
| | |--memmove(&images->legacy_hdr_os_copy, hdr,sizeof(image_header_t));
| | |--images->legacy_hdr_os = hdr;
| | |--images->legacy_hdr_valid = 1;
|--}
|--return buf;

bootm_find_other

bootm 0x5800000 - 0x5c00000

  • 设置images.rd_start = 0
  • 设置images.rd_end = 0
  • 设置images.ft_addr = 0x5c00000
  • 设置images.ft_len= 0x45ea
static int bootm_find_other(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[])
|--return bootm_find_images(flag, argc, argv);
/* find ramdisk */
| |--ret = boot_get_ramdisk(argc, argv, &images, IH_INITRD_ARCH,&images.rd_start, &images.rd_end);
/* find flattened device tree */
| |--ret = boot_get_fdt(flag, argc, argv, IH_ARCH_DEFAULT, &images,&images.ft_addr, &images.ft_len);
| |--set_working_fdt_addr((ulong)images.ft_addr);
| | |--setenv_hex("fdtaddr", addr);

bootm_load_os

  • 解压内核镜像,这里指的是制作uImage时的压缩类型,当前为IH_COMP_NONE

  • 若images.os.load == mages.os.image_start,即bootm传入的image_addr+0x40等于制作uImage时指定的load地址,无需进行memmove

  • 若images.os.load != mages.os.image_start,则将mages.os.image_start上的image镜像拷贝到images.os.load地址上,长度为images.os.image_len

    当前images.os.load=0x8000, mages.os.image_start=0x5800040,需要进行镜像拷贝

if (!ret && (states & BOOTM_STATE_LOADOS))
/* 在准备加载/引导时禁用中断 */
|--ulong iflag = bootm_disable_interrupts()
|--ret = bootm_load_os(images, &load_end, 0);static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end,int boot_progress)
|--int err = image_decomp(os.comp, load, os.image_start, os.type,load_buf, image_buf, image_len,CONFIG_SYS_BOOTM_LEN, &load_end);
/** images.os.load=0x8000,  mages.os.image_start=0x5800040* images.os.image_len = 0x207c28, CONFIG_SYS_BOOTM_LEN=0x1900000*/
| |--if (load == os.image_start)
| | |--break
| |--if (image_len <= CONFIG_SYS_BOOTM_LEN)
/* 将0x5800040上的镜像拷贝到0x8000,拷贝长度为0x207c28 */
| | |--memmove_wd(load_buf, image_buf, image_len, CHUNKSZ);    /* 刷新load范围的d-cache/统一缓存 */
|--flush_cache(load, ALIGN(load_end - load, ARCH_DMA_MINALIGN));
/* 将images->os.load到oad_end的这片内存保留 */
|--lmb_reserve(&images->lmb, images->os.load, (load_end -images->os.load));

boot_ramdisk_high

#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
ulong rd_len = images->rd_end - images->rd_start;
int ret = boot_ramdisk_high(&images->lmb, images->rd_start,rd_len, &images->initrd_start, &images->initrd_end);
if (!ret) {setenv_hex("initrd_start", images->initrd_start);setenv_hex("initrd_end", images->initrd_end);}
#endifint boot_ramdisk_high(struct lmb *lmb, ulong rd_data, ulong rd_len,ulong *initrd_start, ulong *initrd_end)
|--if ((s = getenv("initrd_high")) != NULL)
| |--ulong initrd_high = simple_strtoul(s, NULL, 16);
| |--if (initrd_high == ~0)
| | |--initrd_copy_to_ram = 0;
|--else
| |--initrd_high = getenv_bootm_mapsize() + getenv_bootm_low();|--if (rd_data)
| |--if (!initrd_copy_to_ram)
| |--*initrd_start = rd_data;
| |--*initrd_end = rd_data + rd_len;
| |--lmb_reserve(lmb, rd_data, rd_len);
|--else
| |--if (initrd_high)
| | |--*initrd_start = (ulong)lmb_alloc_base(lmb,rd_len, 0x1000, initrd_high);
| |--else
| | |--*initrd_start = (ulong)lmb_alloc(lmb, rd_len,0x1000);
| |--*initrd_end = *initrd_start + rd_len;
| |--memmove_wd((void *)*initrd_start,(void *)rd_data, rd_len, CHUNKSZ);
|--else
| |--*initrd_start = 0;
| |--*initrd_end = 0;

bootm指定ramdisk_addr

  • setenv initrd_high 不等于~0

    从lmb.memory.region[0]中申请内存,且内存不高于initrd_high,并拷贝镜像

    images->initrd_start = (ulong)lmb_alloc_base(lmb, rd_len, 0x1000, initrd_high);

    images->initrd_end = images->initrd_start + images->rd_end - images->rd_start;

    memmove_wd(void *)*initrd_start, (void *)rd_data, rd_len, CHUNKSZ)

  • setenv initrd_high 0xffffffff,不进行reloacate,保留这块内存

    images->initrd_start = images->rd_start

    images->initrd_end = images->rd_end

    lmb_reserve(lmb, rd_data, rd_len)

  • setenv initrd_high 0

    从lmb.memory.region[0]中任意位置申请内存,并拷贝镜像

    images->initrd_start = (ulong)lmb_alloc(lmb, rd_len, 0x1000);

    mages->initrd_end = images->initrd_start + images->rd_end - images->rd_start;

    memmove_wd(void *)*initrd_start, (void *)rd_data, rd_len, CHUNKSZ)

  • 没有环境变量initrd_high

    initrd_high = bootm_mapsize + bootm_low

nt98566上,bootm未指定ramdisk_addr

images->initrd_start = 0

images->initrd_end = 0

boot_relocate_fdt

/* 只有执行bootm start、bootm fdt子命令才生效 */
#if IMAGE_ENABLE_OF_LIBFDT && defined(CONFIG_LMB)if (!ret && (states & BOOTM_STATE_FDT)) {/* 为设备树中的memory reserve预留内存 */boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr);ret = boot_relocate_fdt(&images->lmb, &images->ft_addr,&images->ft_len);}
#endifint boot_relocate_fdt(struct lmb *lmb, char **of_flat_tree, ulong *of_size)
|--void *fdt_blob = *of_flat_tree;
|--ulong of_len = *of_size + CONFIG_SYS_FDT_PAD;
|--char *fdt_high = getenv("fdt_high")
|--if (fdt_high)
| |--void *desired_addr = (void *)simple_strtoul(fdt_high, NULL, 16);
| |--if (((ulong) desired_addr) == ~0UL)
| | |--void *of_start = fdt_blob;
| | |--lmb_reserve(lmb, (ulong)of_start, of_len);
| | |--int  disable_relocation = 1
| |--else if (desired_addr)
| | |--of_start = (void *)(ulong) lmb_alloc_base(lmb, of_len, 0x1000,(ulong)desired_addr);
| |--else
| | |--of_start = (void *)(ulong) lmb_alloc(lmb, of_len, 0x1000);
|--else
| |--of_start = (void *)(ulong) lmb_alloc_base(lmb, of_len, 0x1000,getenv_bootm_mapsize() + getenv_bootm_low());|--if (disable_relocation)
/* 设置设备树,并进行镜像拷贝 */
| |--fdt_set_totalsize(of_start, of_len);
|--else
| |--int err = fdt_open_into(fdt_blob, of_start, of_len)|--*of_flat_tree = of_start;
|--*of_size = of_len;
|--set_working_fdt_addr((ulong)*of_flat_tree);
| |--setenv_hex("fdtaddr", addr);
  • setenv fdt_high 不等于~0

    从lmb.memory.region[0]中申请内存,且内存不高于initrd_high,并拷贝镜像

    images->ft_addr = (void *)(ulong) lmb_alloc_base(lmb, of_len, 0x1000, (ulong)initrd_high);

    images->ft_len += CONFIG_SYS_FDT_PAD

    memmove

  • setenv fdt_high 0xffffffff, 不进行reloacate,保留这块内存

    images->ft_addr 即bootm 传入的dtb_addr

    images->ft_len += CONFIG_SYS_FDT_PAD

    lmb_reserve

  • setenv fdt_high 0

    从lmb.memory.region[0]中任意位置申请内存

    mages->ft_addr = (void *)(ulong) lmb_alloc(lmb, of_len, 0x1000);

    images->ft_len += CONFIG_SYS_FDT_PAD

  • 没有环境变量fdt_high

    fdt_high = bootm_mapsize + bootm_low

bootm_os_get_boot_func

  • 根据images->os.os获取对应的boot_os_fn,当前images->os.os为IH_OS_LINUX,boot_fn为do_bootm_linux
images->os.os = IH_OS_LINUX
boot_fn = bootm_os_get_boot_func(images->os.os)static boot_os_fn *boot_os[] = {[IH_OS_U_BOOT] = do_bootm_standalone,
#ifdef CONFIG_BOOTM_LINUX[IH_OS_LINUX] = do_bootm_linux,
#endif/* ... */
};boot_os_fn *bootm_os_get_boot_func(int os)
|--return boot_os[os];

do_bootm_linux

  • 执行boot_fn(BOOTM_STATE_OS_PREP,argc, argv, images
  • 执行boot_fn(BOOTM_STATE_OS_GO, argc, argv, images)
if (!ret && (states & BOOTM_STATE_OS_PREP))
|--ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_GO))
|--ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,images, boot_fn);
| |--boot_fn(state, argc, argv, images);int do_bootm_linux(int flag, int argc, char * const argv[],bootm_headers_t *images)
|--if (flag & BOOTM_STATE_OS_PREP)
| |--boot_prep_linux(images);
| |--return 0;
|--boot_prep_linux(images);
|--boot_jump_linux(images, flag);
|--return 0;
boot_prep_linux
  • 设置FDT

    • 为设备树中的reserved-memory预留内存,nt98566中为cma(连续的内存分配器)预留内存,地址为0x01a00000,长度为0

    • 重定位设备树,见boot_relocate_fdt小节

      setenv fdt_high 0xffffffff

      bootm 0x5800000 - 0x5c00000

      mages->ft_addr=0x5c00000

      images->ft_len = 0x45ea /* 区别bootm fdt命令,正常启动不修改其长度(可能是代码bug,此变量在后续启动流程中无用) */

    • 修改设备树,取代传统的TAG

  • 设置ATAGS,从gd->bd->bi_boot_params开始设置要传给内核的TAG信息

static void boot_prep_linux(bootm_headers_t *images)
|--char *commandline = getenv("bootargs");
/* USE FDT,将uboot要向kernel传递的信息修改到设备树, 并将R2寄存器指向该设备树地址*/
|--if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
| |--image_setup_linux(images)
/* 为设备树中的memory reserve预留内存 */
| | |--boot_fdt_add_mem_rsv_regions(lmb, *of_flat_tree);
/* 从lmb.memory.region[0]中申请内存以存放bootargs */
| | |--boot_get_cmdline(lmb, &images->cmdline_start, &images->cmdline_end);
/* 重定位设备树 */
| | |--ret = boot_relocate_fdt(lmb, of_flat_tree, &of_size);
/* 修改设备树,取代传统的TAG */
| | |--ret = image_setup_libfdt(images, *of_flat_tree, of_size, lmb);
| | | |--fdt_root(blob)     /* 修改序列号serial-number */
| | | |--fdt_chosen(blob)   /* 传递bootargs到chosen节点 */
| | | |--arch_fixup_fdt(blob) /* gd->bd->bi_dram[bank]修改设备树中的memory节点 */
| | | |--fdt_fixup_ethernet(blob);    /* 修改设备树中的mac-address */
| | | |--ft_board_setup(blob, gd->bd); /* 空 */
| | | |--ft_system_setup(blob, gd->bd); /* 空 */
| | | |--fdt_shrink_to_minimum(blob, 0); /* 压缩设备树体积 */
| | | |--lmb_reserve(lmb, (ulong)blob, of_size); /* 重新为设备树保留内存 */
| | | |--fdt_initrd(blob, *initrd_start, *initrd_end); /* 设置chosen中的linux,initrd-start和linux,initrd-end *//* USE ATAGS */
/* tag类型的结构体指针params最初指向bd->bi_boot_params,连续向上偏移hdr.size,直到setup_end_tag,同设备树传递的信息,只是将这些信息放到bd->bi_boot_params地址上,并将R2寄存器指向该地址 */
|--else if (BOOTM_ENABLE_TAGS)
| |--setup_start_tag(gd->bd);
| | |--params = (struct tag *)bd->bi_boot_params;
| | |--params = tag_next (params);
| |--setup_serial_tag(&params);
| | |--params->u.serialnr.low = serialnr.low;
| | |--params->u.serialnr.high= serialnr.high;
| | |--params = tag_next (params);
| |--setup_commandline_tag(gd->bd, commandline);
| |--setup_revision_tag(&params);
| |--setup_memory_tags(gd->bd);
| |--setup_initrd_tag(gd->bd, images->initrd_start, images->initrd_end);
| |--setup_initrd_tag(gd->bd, images->rd_start, images->rd_end);
| |--setup_board_tags(&params);
| |--setup_end_tag(gd->bd);
params全局变量
/* u-boot-2019.04/arch/arm/lib/bootm.c */
static struct tag *params;/* arch/arm/include/asm/setup.h */
struct tag_header {u32 size;u32 tag;
};struct tag_core {u32 flags;       /* bit 0 = read-only */u32 pagesize;u32 rootdev;
};struct tag_mem32 {u32 size;u32    start;  /* physical start address */
};
/* ... */struct tag {struct tag_header hdr;union {struct tag_core       core;struct tag_mem32   mem;struct tag_videotext    videotext;struct tag_ramdisk    ramdisk;struct tag_initrd   initrd;struct tag_serialnr  serialnr;struct tag_revision    revision;struct tag_videolfb    videolfb;struct tag_cmdline cmdline;/** Acorn specific*/struct tag_acorn    acorn;/** DC21285 specific*/struct tag_memclk   memclk;} u;
};
boot_jump_linux
  • 禁止中断,关闭cache,关闭mmu

  • 跳转到内核的入口地址images->ep

    #ifdef CONFIG_ARM64

    • CONFIG_ARMV8_SWITCH_TO_EL1:

    armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0, (u64)switch_to_el1, ES_TO_AARCH64);

    • 32-bit OS

    armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number, (u64)images->ft_addr, 0, (u64)images->ep, ES_TO_AARCH32);

    • 64-bit OS

    armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0, images->ep, ES_TO_AARCH64);

    #else

    kernel_entry = (void (*)(int, int, uint))images->ep;

    machid = gd->bd->bi_arch_numberstrict_strtoul(env_get("machid"), 16, &machid)

    FDT: r2 = (unsigned long)images->ft_addr;

    ATAGS: r2 = gd->bd->bi_boot_params;

    kernel_entry(0, machid, r2);

static void boot_jump_linux(bootm_headers_t *images, int flag)/* ARM64 */
#ifdef CONFIG_ARM64
|--void (*kernel_entry)(void *fdt_addr, void *res0, void *res1, void *res2);
|--kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,void *res2))images->ep;
|--announce_and_cleanup(fake);
/* 禁止中断,关闭cache,关mmu */
| |--cleanup_before_linux();
| | |--disable_interrupts();
| | |--dcache_disable();
| | |--icache_disable();
| | |--invalidate_icache_all();
| | |--cpu_cache_initialization
/* flush cache before swtiching to EL2 */
|--do_nonsec_virt_switch();
| |--smp_kick_all_cpus();
| |--dcache_disable();#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
| |--armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,(u64)switch_to_el1, ES_TO_AARCH64);#else
| |--if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&(images->os.arch == IH_ARCH_ARM))
| | |--armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,(u64)images->ft_addr, 0,(u64)images->ep,ES_TO_AARCH32);
| |--else
| | |--armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,images->ep,ES_TO_AARCH64);    #endif/* ARM */
#else
|--unsigned long machid = gd->bd->bi_arch_number;
|--void (*kernel_entry)(int zero, int arch, uint params);
|--kernel_entry = (void (*)(int, int, uint))images->ep;
|--char *s = getenv("machid");
| |--strict_strtoul(s, 16, &machid)
|--announce_and_cleanup(fake);
|--if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
| |--unsigned long r2 = (unsigned long)images->ft_addr;
|--else
| |--r2 = gd->bd->bi_boot_params;
|--kernel_entry(0, machid, r2);

当前images中的值

以NT98566平台为例:

  • 片内SDRAM 128M
  • setenv bootm_low = 0x0;
  • setenv bootm_size = 0x02000000
  • setenv bootm_mapsize = 0x02000000
  • setenv fdt_high = 0xffffffff
  • bootm 0x5800000 - 0x5c00000
成员
image_header_t *legacy_hdr_os; 0x5800000
image_header_t legacy_hdr_os_copy; memmove(&images->legacy_hdr_os_copy, 0x5800000, sizeof(image_header_t));
ulong legacy_hdr_valid; 1
ulong os.start 0x5800000
ulong os.end 0x5a07c68
ulong os.image_start 0x5800040
ulong os.image_len 0x207c28
ulong os.load 0x8000
uint8_t os.comp 00 IH_COMP_NONE
uint8_t os.type 02 IH_TYPE_KERNEL
uint8_t os.os 05 IH_OS_LINUX
uint8_t os.arch 02 IH_ARCH_ARM
ulong ep 0x8000
ulong rd_start 0
ulong rd_end 0
char *ft_addr 0x5c00000
ulong ft_len 0x45ea
ulong initrd_start 0
ulong initrd_end 0
ulong cmdline_start 0
ulong cmdline_end 0
bd_t *kbd 0
int verify 0
int state; BOOTM_STATE_START
lmb.memory.cnt 1
lmb.memory.size 0
lmb.memory.region[0].base 0x0
lmb.memory.region[0].size 0x2000000
lmb.reserved.cnt 3
lmb.reserved.size 0
lmb.reserved.region[0].base 0x8000 /* 为image镜像预留内存 */
lmb.reserved.region[0].size 0x207c28 /* image镜像大小 */
lmb.reserved.region[1].base 1a00000 /* 设备树中reserved-memory */
lmb.reserved.region[2].size 0
lmb.reserved.region[0].base 5c00000 /* 为设备树预留内存 */
lmb.reserved.region[0].size 0x5000 /* 设备树执行fdt_shrink_to_minimum(blob, 0)后压缩过的大小 */

优化启动时间

  • 关闭image的内容打印、加载信息
/* 例如注释以下函数 */
image_print_contents
  • 若直接从flash分区加载image镜像,可以先读0x40(具体根据flash的block大小确定)字节头部,从头部获取image镜像大小,再加载image镜像,避免读取整块flash分区
sprintf(cmd, "mmc read 0x%x 0x%llx 0x%x", image_addr, part_off, 1)
run_command(cmd, 0)
hdr = (image_header_t *)image_addr;
size = image_get_data_size(hdr) + sizeof(image_header_t);
align_size = ALIGN_CEIL(size, MMC_MAX_BLOCK_LEN) / MMC_MAX_BLOCK_LEN;
sprintf(cmd, "mmc read 0x%x 0x%llx 0x%x", image_addr, part_off, align_size);
  • 设置环境变量verify=n,不对image镜像的数据进行crc校验,甚至可以只对image进行magic校验
bootm_find_os
|--boot_get_kernel
| |--image_get_kernel
| | |--image_check_magic(hdr)
| | |--image_check_hcrc(hdr)
| | |--if (verify)
| | | |--image_check_dcrc(hdr)
| | |--image_check_target_arch(hdr)bootm_find_other
|--bootm_find_images
| |--boot_get_ramdisk
| | |--image_get_ramdisk
| | | |--image_check_magic
| | | |--image_check_hcrc
| | | |--if (verify)
| | | | |--image_check_dcrc(rd_hdr)
  • 若制作uImage时指定的-e参数,即镜像入口地址为ep,bootm命令传入的镜像地址为image_addr,保持:ep = image_addr + 0x40(sizeof(image_header_t)),以节省image镜像的拷贝时间
bootm_load_os
|--image_decomp| |--if (load == os.image_start)
| | |--break
| |--if (image_len <= CONFIG_SYS_BOOTM_LEN)
/* 将0x5800040上的镜像拷贝到0x8000,拷贝长度为0x1cbb40 */
| | |--memmove_wd(load_buf, image_buf, image_len, CHUNKSZ);
  • 设置setenv fdt_high = 0xffffffff,setenv initrd_high = 0xffffffff,不进行根文件系统和设备树的重定位,节省拷贝时间,参考boot_ramdisk_high和boot_relocate_fdt小节
  • 注释掉uboot中对设备树无用的信息传递,找出设备树节点需要耗时
image_setup_libfdt
|--fdt_root(blob)     /* 修改序列号serial-number */
|--arch_fixup_fdt(blob) /* gd->bd->bi_dram[bank]修改设备树中的memory节点 */
|--fdt_fixup_ethernet(blob);    /* 修改设备树中的mac-address */
|--ft_board_setup(blob, gd->bd); /* 空 */
|--ft_system_setup(blob, gd->bd); /* 空 */
|--fdt_initrd(blob, *initrd_start, *initrd_end); /* 设置chosen中的linux,initrd-start和linux,initrd-end */

Bootm启动流程分析相关推荐

  1. 解析并符号 读取dll_Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  2. Zygote进程启动流程分析

    文中的源代码版本为api23 Zygote进程启动流程分析 先说结论,zygote进程启动过程中主要做了下面这些事情: 启动DVM虚拟机 预加载部分资源,如一些通用类.通用资源.共享库等 启动syst ...

  3. c++builder启动了怎么停止_App 竟然是这样跑起来的 —— Android App/Activity 启动流程分析...

    在我的上一篇文章: AJie:按下电源键后竟然发生了这一幕 -- Android 系统启动流程分析​zhuanlan.zhihu.com 我们分析了系统在开机以后的一系列行为,其中最后一阶段 AMS( ...

  4. SpringBoot启动流程分析(四):IoC容器的初始化过程

    SpringBoot系列文章简介 SpringBoot源码阅读辅助篇: Spring IoC容器与应用上下文的设计与实现 SpringBoot启动流程源码分析: SpringBoot启动流程分析(一) ...

  5. Exynos4412 Uboot 移植(二)—— Uboot 启动流程分析

    uboot启动流程分析如下: 第一阶段: a -- 设置cpu工作模式为SVC模式 b -- 关闭中断,mmu,cache v -- 关看门狗 d -- 初始化内存,串口 e -- 设置栈 f -- ...

  6. bootloader启动流程分析

    bootloader启动流程分析 1.Bootloader的概念和作用 Bootloader是嵌入式系统的引导加载程序,它是系统上电后运行的第一段程序.在完成对系统的初始化任务之后,它会将Flash中 ...

  7. MyBatis启动流程分析

    目录 MyBatis简单介绍 启动流程分析 简单总结 附录 MyBatis内置别名转换 参考 MyBatis简单介绍 MyBatis是一个持久层框架,使用简单,学习成本较低.可以执行自己手写的SQL语 ...

  8. NameNode之启动流程分析

    NameNode启动流程分析 public staticvoid main(Stringargv[]) throws Exception { if (DFSUtil.parseHelpArgument ...

  9. springboot中获得app_Spring Boot 应用程序启动流程分析

    SpringBoot 有两个关键元素: @SpringBootApplication SpringApplication 以及 run() 方法 SpringApplication 这个类应该算是 S ...

最新文章

  1. Android USB Host与HID通讯
  2. .NET 大会今日开幕 |这些白嫖福利不看肠子都悔青
  3. 大数据_Flink_数据处理_运行时架构7_程序结构和数据流图---Flink工作笔记0022
  4. 雷军是这样评价马斯克的
  5. 编程中怎样将列表中数字排序_R编程中的列表
  6. npm和yarn科学设置淘宝镜像
  7. ubuntu16.04多GPU风扇转速调整
  8. Windows DLL 注入技术
  9. yyds,Python爬虫从小白到Bigboss全套学习路线+视频+资料
  10. 黑苹果E430c, 安装过程
  11. SpringCloud(一)手把手入门
  12. RK3399 修改android桌面图标默认大小
  13. Unity摄像头仿真调研(svl)
  14. cad2017单段线_CAD中如何绘制多段线
  15. java Date days_JAVA的Date类与Calendar类(常用方法)
  16. fortran matlab eng,[转载]关于MATLAB转FORTRAN的几点注意
  17. 哔哩哔哩2020校园招聘前端笔试卷(一)答案解析
  18. python(十二)Uiautomator2搭建UI自动化框架实战
  19. vista识别内存4g_Windows Vista中的语音识别-我在听
  20. 软件开发常见英文单词

热门文章

  1. 爬虫入门经典(九) | 简单一文教你如何爬取扇贝单词
  2. xposed的总开关
  3. 统计自然语言处理梳理二:句法分析
  4. CAD梦想画图中的的“绘图工具——绘线命令”
  5. AC Leetcode 290. 单词规律
  6. Linux字体相关文件存放的目录位置
  7. Clickhouse 在大数据分析平台 - 留存分析
  8. 怎么用python画螺旋_用Python tu绘制螺旋
  9. twitter授权登录 php,PHP版实现Twitter第三方登录的成功案例
  10. 人脸检测实战进阶:使用 OpenCV 进行活体检测