1、框架

MTK 平台的启动过程经过四个模块,分别是BootRom,Preloader,LK,Kernel.

2 、bootloader到kernel启动总逻辑流程图

3、Boot ROM

Boot ROM的主要功能流程如下:

1)设备上电起来后,跳转到Boot ROM,Boot ROM从Reset Vector 开始执行ARM 核。

2)内部SRAM 中初始化栈

3)初始化nand flash 或 emmc

4)把pre-loader加载起到ISRAM

5)跳到pre-loader

4、Preloader

4.1 Preloader主要功能

1)复位寄存器、堆栈的SP,禁止中断IRQ,建立起C运行环境

2)初始化Timer、Clock、UART、DDR等关键硬件

3)对代码进行鉴权

4)pre-loader初始化好DRAM后就将lk从flash(nand/emmc)中加载到DRAM中运行

4.2 Preloader- entry

PreLoader  entry位于  mediatek /platform/MTXXXX//Preloader/src/init/init.s

源码流程如下:

./bootloader/preloader/platform/mt6580/src/init/init.s

.section .text.start
....globl _start
.../* set the cpu to SVC32 mode */MRS   r0,cpsr
    BIC r0,r0,#0x1fORR  r0,r0,#0xd3MSR  cpsr,r0
/* disable interrupt */MRS r0, cpsr
    MOV r1, #INT_BITORR r0, r0, r1MSR cpsr_cxsf, r0...
setup_stk :/* setup stack */LDR r0, stackLDR r1, stacksz
...entry :LDR r0, =bldr_args_addr/* 跳转到C代码 main 入口 */ B   main

4.3 Preloader- main

alps/mediatek /platform/$MTXXXX//Preloader/

void main(u32 *arg)
{struct bldr_command_handler handler;u32 jump_addr, jump_arg;/* get the bldr argument */bldr_param = (bl_param_t *)*arg;// 初始化uart mtk_uart_init(UART_SRC_CLK_FRQ, CFG_LOG_BAUDRATE);// 这里干了很多事情,包括各种的平台硬件(timer,pmic,gpio,wdt...)初始化工作.bldr_pre_process();handler.priv = NULL;handler.attr = 0;handler.cb   = bldr_cmd_handler;// 这里是获取启动模式等信息保存到全局变量g_boot_mode和g_meta_com_type 中.BOOTING_TIME_PROFILING_LOG("before bldr_handshake");bldr_handshake(&handler);BOOTING_TIME_PROFILING_LOG("bldr_handshake");// 下面跟 secro img 相关,跟平台设计强相关./* security check */sec_lib_read_secro();sec_boot_check();device_APC_dom_setup();BOOTING_TIME_PROFILING_LOG("sec_boot_check");/* 如果已经实现EL3,那么进行tz预初始化 */
#if CFG_ATF_SUPPORTtrustzone_pre_init();
#endif/* bldr_load_images
此函数要做的事情就是把lk从ROM中指定位置load到DRAM中,开机log中可以看到具体信息:
[PART] load "lk" from 0x0000000001CC0200 (dev) to 0x81E00000 (mem) [SUCCESS]
这里准备好了jump到DRAM的具体地址,下面详细分析.
*/if (0 != bldr_load_images(&jump_addr)) {print("%s Second Bootloader Load Failed\n", MOD);goto error;}/*
该函数的实现体是platform_post_init,这里要干的事情其实比较简单,就是通过
hw_check_battery去判断当前系统是否存在电池(判断是否有电池ntc脚来区分),
如果不存在就陷入while(1)卡住了,所以在es阶段调试有时候
需要接电源调试的,就需要改这里面的逻辑才可正常开机
*/bldr_post_process();// atf 正式初始化,使用特有的系统调用方式实现.
#if CFG_ATF_SUPPORTtrustzone_post_init();
#endif/* 跳转传入lk的参数,包括boot time/mode/reason 等,这些参数在platform_set_boot_args 函数获取。
*/jump_arg = (u32)&(g_dram_buf->boottag);/* 执行jump系统调用,从 pre-loader 跳转到 lk执行,

5、lk

5.1 lk主要功能

1)从Preloader 获取参数

2)MMU cache 使能

3)外设初始化

4)设置boot 模式

5)加载kernel

6) 跳转到kernel

5.2 lk-reset


lk执行入口:

位于.text.boot 这个section(段),具体定义位置为:

./lk/arch/arm/system-onesegment.ld:10:  .text.boot : { *(.text.boot) }
./lk/arch/arm/system-twosegment.ld:10:  .text.boot : { *(.text.boot) }

该段的代码执行入口是crt0.S文件,位置为:

./lk/arch/arm/crt0.S

crt0.S 中会经过一系列的初始化准备操作,最终跳转到C代码入口kmain函数开始执行,这个是 我们需要重点分析关注的,kmain的位置:

./lk/kernel/main.c

5.3 lk-main

5.4代码分析

1、crt0.S.section ".text.boot"...

.Lstack_setup:/* ==set up the stack for irq, fi==q, abort, undefined, system/user, and lastly supervisor mode */mrs     r0, cpsr
    bic     r0, r0, #0x1fldr        r2, =abort_stack_toporr     r1, r0, #0x12 // irqmsr     cpsr_c, r1ldr      r13, =irq_save_spot        /* save a pointer to a temporary dumping spot used during irq delivery */orr     r1, r0, #0x11 // fiqmsr     cpsr_c, r1mov      sp, r2orr     r1, r0, #0x17 // abortmsr     cpsr_c, r1mov       sp, r2orr     r1, r0, #0x1b // undefinedmsr     cpsr_c, r1mov       sp, r2orr     r1, r0, #0x1f // systemmsr     cpsr_c, r1mov      sp, r2orr       r1, r0, #0x13 // supervisormsr      cpsr_c, r1mov       sp, r2
...bl       kmain

crt0.S 小结:

这里主要干的事情就是建立fiq/irq/abort等各种模式的stack,初始化向量表,然后切换到管理模式(pre-loader运行在EL3, lk运行在EL1),最后跳转到C代码入口 kmain 执行.

2、kmain :

void kmain(void)
{boot_time = get_timer(0);/* 早期初始化线程池的上下文,包括运行队列、线程链表的建立等,lk架构支持多线程,但是此阶段只有一个cpu处于online,所以也只有一条代码执行路径.*/thread_init_early();/* 架构初始化,包括DRAM,MMU初始化使能,使能协处理器,preloader运行在ISRAM,属于物理地址,而lk运行在DRAM,可以选择开启MMU或者关闭,开启MMU可以加速lk的加载过程.*/arch_early_init();/*平台硬件早期初始化,包括irq、timer,wdt,uart,led,pmic,i2c,gpio等,初始化平台硬件,建立lk基本运行环境。*/platform_early_init();boot_time = get_timer(0);// 这个是保留的空函数.target_early_init();dprintf(CRITICAL, "welcome to lk\n\n");/*执行定义在system-onesegment.ld 描述段中的构造函数,不太清楚具体机制:__ctor_list = .;.ctors : { *(.ctors) }__ctor_end = .;*/call_constructors();//内核堆链表上下文初始化等.heap_init();// 线程池初始化,前提是PLATFORM_HAS_DYNAMIC_TIMER需要支持.thread_init();// dpc系统是什么?据说是一个类似work_queue的东东,dpc的简称是什么就不清楚了.dpc_init();// 初始化内核定时器timer_init();// 创建系统初始化工作线程,执行app初始化,lk把业务部分当成一个app.thread_resume(thread_create("bootstrap2", &bootstrap2, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE));// 使能中断.exit_critical_section();// become the idle threadthread_become_idle();
}

kmain 小结:

。初始化线程池,建立线程管理链表、运行队列等;

。初始化各种平台硬件,包括irq、timer,wdt,uart,led,pmic,i2c,gpio等,建立lk基本运行环境;

。初始化内核heap、内核timer等;

。创建系统初始化主线程,进入bootstrap2执行,使能中断,当前线程进入idle;

3、bootstrap2 分析:

static int bootstrap2(void *arg)
{
...
/*平台相关初始化,包括nand/emmc,LCM显示驱动,启动模式选择,加载logo资源,具体代码流程如下时序图.
*/platform_init();
.../*app初始化,跳转到mt_boot_init入口开始执行,对应的 ".apps" 这个section.
*/apps_init();return 0;
}

这里的 apps_init 跳转机制还有点特别:

extern const struct app_descriptor __apps_start;
extern const struct app_descriptor __apps_end;
void apps_init(void)
{const struct app_descriptor *app;/* 这里具体干了什么?如何跳转到mt_boot_init入口?有点不知所云 依次遍历 从__apps_start 到__apps_end 又是什么东东?*/for (app = &__apps_start; app != &__apps_end; app++) {if (app->init)app->init(app);}...
}

这个__apps_start 跟 __apps_end哪里定义的? 是怎么回事呢? 这里就需要了解一点编译链接原理跟memory 布局的东东, 这个实际上是指memory中的一个只读数据段的起始&结束地址区间, 它定义在这个文件中:

./lk/arch/arm/system-onesegment.ld:47:       __apps_start = .;.rodata : {
.... = ALIGN(4);__apps_start = .;KEEP (*(.apps))__apps_end = .;. = ALIGN(4); __rodata_end = . ;
}

该mem地址区间是[__apps_start, __apps_end],显然区间就是“.apps” 这个section内容了. 那么这个section是在哪里初始化的呢?继续看:

./lk/app/mt_boot/mt_boot.c:1724:APP_START(mt_boot)
.init = mt_boot_init,APP_END

展开APP_START:

#define APP_START(appname) struct app_descriptor _app_##appname __SECTION(".apps") = { .name = #appname,
#define APP_END };

到这里就很明显了,编译链接系统会将mt_boot_init这个地址记录到".apps"这个section中!所以下面代码要干的事情就很清晰了,执行app->init(app)后就等价于调用了void mt_boot_init(const struct app_descriptor *app) 函数.

for (app = &__apps_start; app != &__apps_end; app++) {if (app->init)app->init(app);
}

bootstrap2 函数小结:

。平台相关初始化,包括nand/emmc,显现相关驱动,启动模式选择,加载logo资源 检测是否DA模式,检测分区中是否有KE信息,如果就KE信息,就从分区load 到DRAM, 点亮背光,显示logo,禁止I/D-cache和MMU,跳转到DA(??),配置二级cache的size 获取bat电压,判断是否低电量是否显示充电logo等,总之此函数干的事情比较多.时序图(platform_init)可以比较清晰直观的描述具体细节

。跳转到到mt_boot_init函数,对应的 ".apps" 这个section,相关机制上面已经详细描述,不再复述.

4、mt_boot_init 分析

void mt_boot_init(const struct app_descriptor *app)
{unsigned usb_init = 0;unsigned sz = 0;int sec_ret = 0;char tmp[SN_BUF_LEN+1] = {0};unsigned ser_len = 0;u64 key;u32 chip_code;char serial_num[SERIALNO_LEN];/* 获取串号字符串 */key = get_devinfo_with_index(13);key = (key << 32) | (unsigned int)get_devinfo_with_index(12);/* 芯片代码 */chip_code = board_machtype();if (key != 0)get_serial(key, chip_code, serial_num);elsememcpy(serial_num, DEFAULT_SERIAL_NUM, SN_BUF_LEN);/* copy serial from serial_num to sn_buf */memcpy(sn_buf, serial_num, SN_BUF_LEN);dprintf(CRITICAL,"serial number %s\n",serial_num);/* 从特定分区获取产品sn号,如果获取失败就使用默认值 DEFAULT_SERIAL_NUM */
#ifdef SERIAL_NUM_FROM_BARCODEser_len = read_product_info(tmp);if (ser_len == 0) {ser_len = strlen(DEFAULT_SERIAL_NUM);strncpy(tmp, DEFAULT_SERIAL_NUM, ser_len);}memset( sn_buf, 0, sizeof(sn_buf));strncpy( sn_buf, tmp, ser_len);
#endifsn_buf[SN_BUF_LEN] = '\0';surf_udc_device.serialno = sn_buf;/* mtk平台默认不支持 fastboot */if (g_boot_mode == FASTBOOT)goto fastboot;/* secure boot相关 */
#ifdef MTK_SECURITY_SW_SUPPORT
#if MTK_FORCE_VERIFIED_BOOT_SIG_VFYg_boot_state = BOOT_STATE_RED;
#elseif (0 != sec_boot_check(0)) {g_boot_state = BOOT_STATE_RED;}
#endif
#endif/* 这里干的事情就比较多了,跟进g_boot_mode选择各种启动模式,例如:
normal、facotry、fastboot、recovery等,然后从ROM中的boot.img分区找到(解压)
ramdisk跟zImage的地址loader到DRAM的特定地址中,kernel最终load到DRAM中的地址
(DRAM_PHY_ADDR + 0x8000) == 0x00008000.
read the data of boot (size = 0x811800)
*/boot_linux_from_storage();fastboot:target_fastboot_init();if (!usb_init)/*Hong-Rong: wait for porting*/udc_init(&surf_udc_device);mt_part_dump();sz = target_get_max_flash_size();fastboot_init(target_get_scratch_address(), sz);udc_start();}

mt_boot_init 分析小结:

。获取设备串号字符串、芯片代码、sn号等.

。如果实现了secure boot则进行sec boot的check工作;

。进入 boot_linux_from_storage 函数初始化,该函数很重要,干了很多事情,如下分析.

5、boot_linux_from_storage 分析:

int boot_linux_from_storage(void)
{int ret=0;
...switch (g_boot_mode) {case NORMAL_BOOT:case META_BOOT:case ADVMETA_BOOT:case SW_REBOOT:case ALARM_BOOT:case KERNEL_POWER_OFF_CHARGING_BOOT:case LOW_POWER_OFF_CHARGING_BOOT:
                        /* 检查boot分区的头部是否有bootopt标识,如果没有就报错 */ret = mboot_android_load_bootimg_hdr("boot", CFG_BOOTIMG_LOAD_ADDR);if (ret < 0) {msg_header_error("Android Boot Image");}                       /* 64bit & 32bit kimg地址获取不一样*/if (g_is_64bit_kernel) {kimg_load_addr = (unsigned int)target_get_scratch_address();} else {kimg_load_addr = (g_boot_hdr!=NULL) ? g_boot_hdr->kernel_addr : CFG_BOOTIMG_LOAD_ADDR;}                       /* 从EMMC的boot分区取出bootimage载入到DRAM  dprintf(CRITICAL, " > from - 0x%016llx (skip boot img hdr)\n",start_addr);dprintf(CRITICAL, " > to   - 0x%x (starts with kernel img hdr)\n",addr);len = dev->read(dev, start_addr, (uchar*)addr, g_bimg_sz); <<= 系统调用load到DRAM开机log:[3380]  > from - 0x0000000001d20800 (skip boot img hdr)[3380]  > to   - 0x80008000 (starts with kernel img hdr)*/ret = mboot_android_load_bootimg("boot", kimg_load_addr);if (ret < 0) {msg_img_error("Android Boot Image");}dprintf(CRITICAL,"[PROFILE] ------- load boot.img takes %d ms -------- \n", (int)get_timer(time_load_bootimg));break;case RECOVERY_BOOT:
...break;case FACTORY_BOOT:case ATE_FACTORY_BOOT:
...break;
...}/* 重定位根文件系统(ramdisk)地址 */memcpy((g_boot_hdr!=NULL) ? (char *)g_boot_hdr->ramdisk_addr : (char *)CFG_RAMDISK_LOAD_ADDR, (char *)(g_rmem_off), g_rimg_sz);g_rmem_off = (g_boot_hdr!=NULL) ? g_boot_hdr->ramdisk_addr : CFG_RAMDISK_LOAD_ADDR;.../* 传入cmdline,设置selinux */
#if SELINUX_STATUS == 1cmdline_append("androidboot.selinux=disabled");
#elif SELINUX_STATUS == 2cmdline_append("androidboot.selinux=permissive");
#endif/* 准备启动linux kernel */boot_linux((void *)CFG_BOOTIMG_LOAD_ADDR, (unsigned *)CFG_BOOTARGS_ADDR,(char *)cmdline_get(), board_machtype(), (void *)CFG_RAMDISK_LOAD_ADDR, g_rimg_sz);while (1) ;return 0;
}

boot_linux_from_storage 小结:

。跟据g_boot_mode选择各种启动模式,例如: normal、facotry、fastboot、recovery等,然后从EMMC中的boot分区找到(解压) ramdisk跟zImage的地址通过read系统调用load到DRAM址中, kernel最终load到DRAM的地址:(DRAM_PHY_ADDR + 0x8000);

。重定位根文件系统地址;

。跳转到 boot_linux,正式拉起kernel;

6、boot_linux 分析:

boot_linux 实际上跑的是boot_linux_fdt,这个函数有对dtb的加载做出来,期间操作相当复杂,这里只简单关注主流程.

void boot_linux(void *kernel, unsigned *tags,char *cmdline, unsigned machtype,void *ramdisk, unsigned ramdisk_size)
{
...
// 新架构都是走fdt分支.
#ifdef DEVICE_TREE_SUPPORTboot_linux_fdt((void *)kernel, (unsigned *)tags,(char *)cmdline, machtype,(void *)ramdisk, ramdisk_size);while (1) ;
#endif
...int boot_linux_fdt(void *kernel, unsigned *tags,char *cmdline, unsigned machtype,void *ramdisk, unsigned ramdisk_size)
{
...void (*entry)(unsigned,unsigned,unsigned*) = kernel;
...// find dt from kernel imgif (fdt32_to_cpu(*(unsigned int *)dtb_addr) == FDT_MAGIC) {dtb_size = fdt32_to_cpu(*(unsigned int *)(dtb_addr+0x4));} else {dprintf(CRITICAL,"Can't find device tree. Please check your kernel image\n");while (1) ;}
...if (!has_set_p2u) {
/* 控制进入kernel后uart的输出,非eng版本默认是关闭的,如果调试需要就可以改这里为"printk.disable_uart=0"*/#ifdef USER_BUILDsprintf(cmdline,"%s%s",cmdline," printk.disable_uart=1");
#elsesprintf(cmdline,"%s%s",cmdline," printk.disable_uart=0 ddebug_query=\"file *mediatek* +p ; file *gpu* =_\"");
#endif
...}...// led,irq关闭platform_uninit();// 关闭I/D-cache,关闭MMU,今天kernel的条件.arch_disable_cache(UCACHE);arch_disable_mmu();// sec initextern void platform_sec_post_init(void)__attribute__((weak));if (platform_sec_post_init) {platform_sec_post_init();}// 如果是正在充电,检测到power key后执行reset.if (kernel_charging_boot() == 1) {if (pmic_detect_powerkey()) {dprintf(CRITICAL,"[%s] PowerKey Pressed in Kernel Charging Mode Before Jumping to Kernel, Reboot Os\n", __func__);mtk_arch_reset(1);}}
#endif
...// 输出关键信息。dprintf(CRITICAL,"cmdline: %s\n", cmdline);dprintf(CRITICAL,"lk boot time = %d ms\n", lk_t);dprintf(CRITICAL,"lk boot mode = %d\n", g_boot_mode);dprintf(CRITICAL,"lk boot reason = %s\n", g_boot_reason[boot_reason]);dprintf(CRITICAL,"lk finished --> jump to linux kernel %s\n\n", g_is_64bit_kernel ? "64Bit" : "32Bit");// 执行系统调用,跳转到kernel,这里的entry实际上就是前面的kernel在DRAM的入口地址.if (g_is_64bit_kernel) {lk_jump64((u32)entry, (u32)tags, 0, KERNEL_64BITS);} else {dprintf(CRITICAL,"[mt_boot] boot_linux_fdt entry:0x%08x, machtype:%d\n",entry,machtype);entry(0, machtype, tags);}while (1);return 0;
}

开机log打印信息:

[4260] cmdline: console=tty0 console=ttyMT0,921600n1 root=/dev/ram vmalloc=496M androidboot.hardware=mt6580 androidboot.verifiedbootstate=green bootopt=64S3,32S1,32S1 printk.disable_uart=1 bootprof.pl_t=1718 bootprof.lk_t=2178 boot_reason=0 androidboot.serialno=0123456789ABCDEF androidboot.bootreason=power_key gpt=1[4260] lk boot time = 2178 ms[4260] lk boot mode = 0[4260] lk boot reason = power_key[4260] lk finished --> jump to linux kernel 32Bit[4260] [mt_boot] boot_linux_fdt entry:0x80008000, machtype:6580

boot_linux 小结:

。初始化DTB(device tree block);

。准备各种cmdline参数传入kernel;

。关闭I/D-cache、MMU;

。打印关键信息,正式拉起kernel.

bootloader两个阶段就分析完了!

MTK 驱动开发(5)---bootloader相关推荐

  1. 详解关于MTK驱动开发学习教程

    MTK驱动开发学习教程是本文要介绍的内容,主要是来了解MTK的驱动开发的过程,文章中很详细的讲解了这个问题,具体内容来看本文详解. 一.Charge Parameters. 1.相关文件chr_par ...

  2. MTK 驱动开发(33)---Vibrator

    Vibrator 驱动开发相对比较简单 1.配置功能及参数 vibrator0:vibrator@0 {compatible = "mediatek,vibrator";vib_t ...

  3. MTK 驱动开发(32)---Sensor 移植及调试2

    接续上一节,本文主要介绍驱动部分的客制化 3. Sensor Driver 的客制化 主要涉及三个方面: 1)配置 codegen.dws ---I2C 地址.eint.gpio 2)配置驱动参数 3 ...

  4. MTK 驱动开发(48)---ARM 看门狗机制

    ARM 看门狗机制 [包括MTK] SYS_LAST_KMSG里的hw_status和fiq step的含义 阅读数:559 [DESCRIPTION] SYS_LAST_KMSG这支文件是记录上次重 ...

  5. MTK 驱动开发(42)---GAT 工具使用

    GAT 工具介绍: 1.关于GAT GAT是MTK在DDMS基础上进行二次开发封装的一个集多种debug功能为一体的工具,除了包含原有DDMS的功能以外还支持kernel抓取,获取native进程列表 ...

  6. MTK 驱动开发(41)---MTK 调试工具

    MTK Android software Tools工具的说明 MTK发布的Android software Tools工具包,里面包含了很多的MTK工具,如下是简要说明及学习文档 MTK Andro ...

  7. MTK 驱动开发(35)---待机功耗分析流程

    1.目的 2.MTK平台各个场景功耗数据测试方法 很多功耗问题都是因为测试手法不对,列出一些常用场景功耗测试手法.  测试功耗数据之前,请先确认以下配置:  1.关闭 WIFI/BT/GPS,关闭数据 ...

  8. MTK 驱动开发(34)---待机功耗调试

    1.概要 待机平均电流非常容易出问题,也很难分析理清楚,应为涉及APK/Modem/Wifi这些不确定的因素,这类问题一定要遵循一个处理原则,到底在出现啥样的环境下复现,做几个实验,给出清晰的问题描述 ...

  9. MTK 驱动开发(30)---Memory 移植

    一.MTK 平台和高通平台在器件选型时都要求选择已经验证过的器件,第一步需要QVL验证 1) 根据硬件原理图和EMMC 和DDR厂家的芯片资料, 确定EMMC 和DDR  64+4,型号如下: EMM ...

最新文章

  1. 排序算法三:插入排序
  2. 高精度减法(C++实现)
  3. LeetCode_104.二叉树的最大深度
  4. RxJava中BehaviorSubject适合的使用场景
  5. hdu 4885 (n^2*log(n)推断三点共线建图)+最短路
  6. 我的世界网易怎么下载java材质包_我的世界中国版材质包怎么用 材质包设置
  7. 一份详尽的利用 Kubeadm部署 Kubernetes 1.13.1 集群指北
  8. 第一次马拉松_成为数据科学家是一场马拉松而不是短跑
  9. 世上最简单的mysql_最简单易懂的mysql安装教程
  10. mysql视图执行原理_MySql中的视图 触发器 存储过程,以及事物
  11. 与基础事务管理器的通信失败 存货申请_干货必读!细说分布式事务两阶段提交...
  12. GradView使用举例
  13. 多线程之wait和notify使用注意事项
  14. win10专业版虚拟机配置服务器,如何在Win10专业版中添加Hyper-V虚拟机?
  15. python爬取网易云音乐_爬取网易云音乐评论(一)——用python执行JS脚本
  16. 【带权并查集经典例题】银河英雄传说【同POJ 1988 cube stacking】
  17. CPLEX求解器入门案例
  18. cad一键卸载工具叫什么_CAD专用卸载修复工具,一键完全彻底卸载删除CAD软件的专用卸载工具...
  19. 一些可以使用的网上图片地址
  20. 数据结构练习题——线性表(二)

热门文章

  1. html 表格 单击,在HTML表格中单击“空”单元格
  2. oracle字段去重查询,oracle怎么去重查询
  3. 力扣812.最大三角形面积
  4. java基础第五篇封装与面向对象
  5. Web前端-Vue.js必备框架(一)
  6. 生活记录--考研日记(1)
  7. 从零开始搭建vue移动端项目到上线的步骤
  8. clion上添加程序的预定添加程序的命令行
  9. Selenium with Python 006 - 操作浏览器
  10. vue从入门到开发--4--处理http请求