Mini440之uboot移植流程之linux内核启动分析(六)
2022/1/18 7:03:49
本文主要是介绍Mini440之uboot移植流程之linux内核启动分析(六),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
在前面的章节关于u-boot的源码,以及u-boot的移植这一块我们介绍完了。接下来,我们开始进入第二个阶段,linux内核移植,以及驱动开发。
在这之前,我们遗漏了u-boot中的一个重要环节没有介绍,就是u-boot如何执行bootm命令,如何实现linux内核启动。
一、linux内核启动入口之do_bootm
我们在Mini440之uboot移植之源码分析命令解析(五) 介绍过如果配置了CONFIG_BOOTCOMMAND宏:
#define CONFIG_BOOTCOMMAND "nand read 0x30000000 kernel; bootm 0x30000000" //bootcmd
那么在执行autoboot_command函数的时候,将会执行该命令。
bootm这个命令用于启动一个操作系统映射,它会从映射文件的头部取得一些信息,这些信息包括:映射文件的基于的cpu架构、其操作系统类型、映射的类型、压缩方式、映射文件在内存中的加载地址、映射文件运行的入口地址、映射文件名等。
nand read 0x30000000命令:这里将NAND kernel分区的代码加载到地址0x30000000;
bootm 0x3000000:启动linux内核;
1.1 autoboot_command(common/autoboot.c)
void autoboot_command(const char *s) { debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>"); if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) { run_command_list(s, -1, 0); } }
如果在u-boot启动倒计时结束之前,没有按下任何键,将会执行那么将执行run_command_list,此函数会执行参数s指定的一系列命令,也就是bootcmd中配置中的命令,bootcmd中保存着默认的启动命令。
在默认环境变量default_environment中定义有:
#ifdef CONFIG_BOOTCOMMAND "bootcmd=" CONFIG_BOOTCOMMAND "\0" #endif
1.2 do_bootm(cmd/bootm.c)
由于要执行bootm命令,所以我们需要打开与bootm命令相关的文件进行分析,bootm命令定义在cmc/bootm.c文件中:
U_BOOT_CMD( bootm, CONFIG_SYS_MAXARGS, 1, do_bootm, "boot application image from memory", bootm_help_text );
找到对应的do_bootm函数,去除无用的代码:
/*******************************************************************/ /* bootm - boot application image from image in memory */ /*******************************************************************/ 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); /* endp pointing to NULL means that argv[0] was just a * valid number, pass it along to the normal bootm processing * * If endp is ':' or '#' assume a FIT identifier so pass * along for normal processing. * * Right now we assume the first arg should never be '-' */ 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 | BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO, &images, 1); }
当执行bootm 0x30000000,函数入参:第一个参数是bootm命令结构体,flag是命令标志位,argv[0]='"bootm"、argv[1]="0x3000000",argc=2。
这里进入函数之后argc=1,argv[0]=0x30000000.
bootm的核心是do_bootm_states,以全局变量bootm_headers_t images作为do_bootm_states的参数,改变量在cmd/bootm.c文件中声明。
bootm_headers_t images; /* pointers to os/initrd/fdt images */
bootm会根据参数以及参数指向的镜像来填充这个结构体里面的成员。 最终再使用这个结构体里面的信息来填充kernel启动信息并且到跳转到kernel中。
下面详细说明这个函数。
二、do_bootm_states(cmd/bootm.c)
我们先来看一下这个函数的声明:
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[], int states, bootm_headers_t *images, int boot_progress);
2.1 bootm_headers_t(include/image.h)
bootm_headers_t是一个复杂的数据结构,官网是这样描述的:
/* * 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; #if IMAGE_ENABLE_FIT const char *fit_uname_cfg; /* configuration node unit name */ void *fit_hdr_os; /* os FIT image header */ const char *fit_uname_os; /* os subimage node unit name */ int fit_noffset_os; /* os subimage node offset */ void *fit_hdr_rd; /* init ramdisk FIT image header */ const char *fit_uname_rd; /* init ramdisk subimage node unit name */ int fit_noffset_rd; /* init ramdisk subimage node offset */ void *fit_hdr_fdt; /* FDT blob FIT image header */ const char *fit_uname_fdt; /* FDT blob subimage node unit name */ int fit_noffset_fdt;/* FDT blob subimage node offset */ void *fit_hdr_setup; /* x86 setup FIT image header */ const char *fit_uname_setup; /* x86 setup subimage node name */ int fit_noffset_setup;/* x86 setup subimage node offset */ #endif #ifndef USE_HOSTCC image_info_t os; /* os image info */ ulong ep; /* entry point of OS */ ulong rd_start, rd_end;/* ramdisk start/end */ char *ft_addr; /* flat dev tree address */ ulong ft_len; /* length of flat device tree */ ulong initrd_start; ulong initrd_end; ulong cmdline_start; ulong cmdline_end; bd_t *kbd; #endif int verify; /* getenv("verify")[0] != 'n' */ #define BOOTM_STATE_START (0x00000001) #define BOOTM_STATE_FINDOS (0x00000002) #define BOOTM_STATE_FINDOTHER (0x00000004) #define BOOTM_STATE_LOADOS (0x00000008) #define BOOTM_STATE_RAMDISK (0x00000010) #define BOOTM_STATE_FDT (0x00000020) #define BOOTM_STATE_OS_CMDLINE (0x00000040) #define BOOTM_STATE_OS_BD_T (0x00000080) #define BOOTM_STATE_OS_PREP (0x00000100) #define BOOTM_STATE_OS_FAKE_GO (0x00000200) /* 'Almost' run the OS */ #define BOOTM_STATE_OS_GO (0x00000400) int state; #ifdef CONFIG_LMB struct lmb lmb; /* for memory mgmt */ #endif } bootm_headers_t;
bootm_headers_t用于Legacy或设备树(FDT)方式镜像的启动,其中包括了os/initrd/fdt images的信息。我们这里大概介绍一下和这个结构体的成员变量:
- legacy_hdr_os:Legacy-uImage的镜像头;
- legacy_hdr_os_copy:Legacy-uImage的镜像头备份;
- fit_uname_cfg:配置节点名;
- fit_hdr_os:FIT-uImage中kernel镜像头;
- fit_uname_os:FIT-uImag中kernel的节点名;
- fit_noffset_os:FIT-uImage中kernel的节点偏移;
- fit_hdr_rd:FIT-uImage中ramdisk的镜像头;
- fit_uname_rd:FIT-uImage中ramdisk的节点名;
- fit_noffset_rd:FIT-uImage中ramdisk的节点偏移;
- fit_hdr_fdt:FIT-uImage中FDT的镜像头;
- fit_uname_fdt:FIT-uImage中FDT的节点名;
- fit_noffset_fdt:FIT-uImage中FDT的节点偏移;
- os:操作系统信息的结构体,比如内核镜像在内存的起始地址、以及大小;
- ep:操作系统的入口地址;
- rd_start:ramdisk在内存上的起始地址;
- rd_end:ramdisk在内存上的结束地址
- ft_addr:fdt在内存上的地址;
- ft_len:fdt在内存上的长度;
- verify:是否需要验证;
- state:状态标识,用于标识对应的bootm需要做什么操作;
其中 legacy_hdr_os是image_header_t类型。这个结构尤为重要,下面来介绍。
2.2 (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 */ __be32 ih_hcrc; /* Image Header CRC Checksum */ __be32 ih_time; /* Image Creation Timestamp */ __be32 ih_size; /* Image Data Size */ __be32 ih_load; /* Data Load Address */ __be32 ih_ep; /* Entry Point Address */ __be32 ih_dcrc; /* Image Data CRC Checksum */ uint8_t ih_os; /* Operating System */ uint8_t ih_arch; /* CPU architecture */ uint8_t ih_type; /* Image Type */ uint8_t ih_comp; /* Compression Type */ uint8_t ih_name[IH_NMLEN]; /* Image Name */ } image_header_t;
比较重要的成员有:
- ih_magic:镜像的魔数,用来给uboot判断是什么格式的镜像(zImage、uImage等);
- ih_ep:镜像的入口;
- inj_os:镜像的系统;
2.3 状态说明
do_bootm_states的state参数是一大堆的标志宏,这些标志宏就是u-boot启动时需要的阶段,每个阶段都有一个宏来表示。
#define BOOTM_STATE_START (0x00000001) #define BOOTM_STATE_FINDOS (0x00000002) #define BOOTM_STATE_FINDOTHER (0x00000004) #define BOOTM_STATE_LOADOS (0x00000008) #define BOOTM_STATE_RAMDISK (0x00000010) #define BOOTM_STATE_FDT (0x00000020) #define BOOTM_STATE_OS_CMDLINE (0x00000040) #define BOOTM_STATE_OS_BD_T (0x00000080) #define BOOTM_STATE_OS_PREP (0x00000100) #define BOOTM_STATE_OS_FAKE_GO (0x00000200) /* 'Almost' run the OS */ #define BOOTM_STATE_OS_GO (0x00000400)
- BOOTM_STATE_START :开始执行bootm的一些准备动作;
- BOOTM_STATE_FINDOS :查找操作系统镜像;
- BOOTM_STATE_FINDOTHER :查找操作系统镜像外的其它镜像,比如FDT、ramdisk等;
- BOOTM_STATE_LOADOS :加载操作系统;
- BOOTM_STATE_RAMDISK :操作ramdisk;
- BOOTM_STATE_FDT :操作FDT;
- BOOTM_STATE_OS_CMDLINE :操作commandline;
- BOOTM_STATE_OS_BD_T :跳转到操作系统的前的准备动作;
- BOOTM_STATE_OS_PREP :执行跳转前的准备动作 ;
- BOOTM_STATE_OS_FAKE_GO :伪跳转,一般都能直接跳转到kernel中去
- BOOTM_STATE_OS_GO :设置启动参数,跳转到kernel所在的地址上 ;
do_bootm_states根据states来判断要执行的操作。在这些流程中,起传递作用的是bootm_headers_t images这个数据结构,有些流程是解析镜像,往这个结构体里写数据。 而跳转的时候,则需要使用到这个结构体里面的数据。
2.4 do_bootm_states函数执行流程
/** * Execute selected states of the bootm command. * * Note the arguments to this state must be the first argument, Any 'bootm' * or sub-command arguments must have already been taken. * * Note that if states contains more than one flag it MUST contain * BOOTM_STATE_START, since this handles and consumes the command line args. * * Also note that aside from boot_os_fn functions and bootm_load_os no other * functions we store the return value of in 'ret' may use a negative return * value, without special handling. * * @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 * @return 0 if ok, something else on error. Some errors will cause this * function to perform a reboot! If states contains BOOTM_STATE_OS_GO * then the intent is to boot an OS, so this function will not return * unless the image type is standalone. */ int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[], int states, bootm_headers_t *images, int boot_progress) { boot_os_fn *boot_fn; ulong iflag = 0; int ret = 0, need_boot_fn; images->state |= states; /* * Work through the states and see how far we get. We stop on * any error. */ if (states & BOOTM_STATE_START) ret = bootm_start(cmdtp, flag, argc, argv); if (!ret && (states & BOOTM_STATE_FINDOS)) ret = bootm_find_os(cmdtp, flag, argc, argv); if (!ret && (states & BOOTM_STATE_FINDOTHER)) { ret = bootm_find_other(cmdtp, flag, argc, argv); argc = 0; /* consume the args */ } /* Load the OS */ if (!ret && (states & BOOTM_STATE_LOADOS)) { ulong load_end; iflag = bootm_disable_interrupts(); ret = bootm_load_os(images, &load_end, 0); if (ret == 0) lmb_reserve(&images->lmb, images->os.load, (load_end - images->os.load)); else if (ret && ret != BOOTM_ERR_OVERLAP) goto err; else if (ret == BOOTM_ERR_OVERLAP) ret = 0; #if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY) if (images->os.os == IH_OS_LINUX) fixup_silent_linux(); #endif } /* Relocate the ramdisk */ #ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH if (!ret && (states & BOOTM_STATE_RAMDISK)) { ulong rd_len = images->rd_end - images->rd_start; 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); } } #endif #if IMAGE_ENABLE_OF_LIBFDT && defined(CONFIG_LMB) if (!ret && (states & BOOTM_STATE_FDT)) { boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr); ret = boot_relocate_fdt(&images->lmb, &images->ft_addr, &images->ft_len); } #endif /* From now on, we need the OS boot function */ if (ret) return ret; boot_fn = bootm_os_get_boot_func(images->os.os); need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE | BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO); if (boot_fn == NULL && need_boot_fn) { if (iflag) enable_interrupts(); printf("ERROR: booting os '%s' (%d) is not supported\n", genimg_get_os_name(images->os.os), images->os.os); bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS); return 1; } /* Call various other states that are not generally used */ if (!ret && (states & BOOTM_STATE_OS_CMDLINE)) ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images); if (!ret && (states & BOOTM_STATE_OS_BD_T)) ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images); if (!ret && (states & BOOTM_STATE_OS_PREP)) ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images); #ifdef CONFIG_TRACE /* Pretend to run the OS, then run a user command */ if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) { char *cmd_list = getenv("fakegocmd"); ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO, images, boot_fn); if (!ret && cmd_list) ret = run_command_list(cmd_list, -1, flag); } #endif /* Check for unsupported subcommand. */ if (ret) { puts("subcommand not supported\n"); return ret; } /* Now run the OS! We hope this doesn't return */ if (!ret && (states & BOOTM_STATE_OS_GO)) ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO, images, boot_fn); /* Deal with any fallout */ err: if (iflag) enable_interrupts(); if (ret == BOOTM_ERR_UNIMPLEMENTED) bootstage_error(BOOTSTAGE_ID_DECOMP_UNIMPL); else if (ret == BOOTM_ERR_RESET) do_reset(cmdtp, flag, argc, argv); return ret; }
代码具体执行流程:
- 初始化images->state|=states;
- states跟宏BOOTM_STATE_START进行与操作,通过执行bootm_start;
- states跟宏BOOTM_STATE_FINDOS进行与操作,通过执行bootm_find_os;
- states跟宏BOOTM_STATE_FINDOTHER进行与操作,通过执行bootm_find_other;
- states跟宏BOOTM_STATE_LOADOS进行与操作,通过关闭中断,执行bootm_load_os;
- states跟宏BOOTM_STATE_OS_PREP进行与操作,通过执行boot_fn;
- states跟宏BOOTM_STATE_OS_GO进行与操作,通过执行boot_selected_os;
boot_selected_os,这函数里面就执行do_bootm_linux跳转到我们的内核去运行了,如无意外,到了这里一般情况下就不返回了。
我们根据状态参数,绘制出这个函数的执行流程:
2.5 bootm_start(common/bootm.c)
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"); boot_start_lmb(&images); bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start"); images.state = BOOTM_STATE_START; return 0; }
代码具体执行流程:
- 清空images结构体;
- 获取环境遍历verify,并赋值给images.verify;
- 执行boot_start_lmb()初始化images.lmb;
- 执行bootstage_mark_name,记录启动阶段的名字;
- 设置images.state = BOOTM_STATE_START;
2.6 bootm_find_os(common/bootm.c)
static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { const void *os_hdr; bool ep_found = false; int ret; /* 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); if (images.os.image_len == 0) { puts("ERROR: can't get kernel image!\n"); return 1; } /* get image parameters */ switch (genimg_get_format(os_hdr)) { #if defined(CONFIG_IMAGE_FORMAT_LEGACY) 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); break; #endif #if IMAGE_ENABLE_FIT case IMAGE_FORMAT_FIT: if (fit_image_get_type(images.fit_hdr_os, images.fit_noffset_os, &images.os.type)) { puts("Can't get image type!\n"); bootstage_error(BOOTSTAGE_ID_FIT_TYPE); return 1; } if (fit_image_get_comp(images.fit_hdr_os, images.fit_noffset_os, &images.os.comp)) { puts("Can't get image compression!\n"); bootstage_error(BOOTSTAGE_ID_FIT_COMPRESSION); return 1; } if (fit_image_get_os(images.fit_hdr_os, images.fit_noffset_os, &images.os.os)) { puts("Can't get image OS!\n"); bootstage_error(BOOTSTAGE_ID_FIT_OS); return 1; } if (fit_image_get_arch(images.fit_hdr_os, images.fit_noffset_os, &images.os.arch)) { puts("Can't get image ARCH!\n"); return 1; } images.os.end = fit_get_end(images.fit_hdr_os); if (fit_image_get_load(images.fit_hdr_os, images.fit_noffset_os, &images.os.load)) { puts("Can't get image load address!\n"); bootstage_error(BOOTSTAGE_ID_FIT_LOADADDR); return 1; } break; #endif #ifdef CONFIG_ANDROID_BOOT_IMAGE case IMAGE_FORMAT_ANDROID: images.os.type = IH_TYPE_KERNEL; images.os.comp = IH_COMP_NONE; images.os.os = IH_OS_LINUX; images.os.end = android_image_get_end(os_hdr); images.os.load = android_image_get_kload(os_hdr); images.ep = images.os.load; ep_found = true; break; #endif default: puts("ERROR: unknown image format type!\n"); return 1; } /* If we have a valid setup.bin, we will use that for entry (x86) */ if (images.os.arch == IH_ARCH_I386 || images.os.arch == IH_ARCH_X86_64) { ulong len; ret = boot_get_setup(&images, IH_ARCH_I386, &images.ep, &len); if (ret < 0 && ret != -ENOENT) { puts("Could not find a valid setup.bin for x86\n"); return 1; } /* Kernel entry point is the setup.bin */ } else if (images.legacy_hdr_valid) { images.ep = image_get_ep(&images.legacy_hdr_os_copy); #if IMAGE_ENABLE_FIT } else if (images.fit_uname_os) { int ret; ret = fit_image_get_entry(images.fit_hdr_os, images.fit_noffset_os, &images.ep); if (ret) { puts("Can't get entry point property!\n"); return 1; } #endif } else if (!ep_found) { puts("Could not find kernel entry point!\n"); return 1; } if (images.os.type == IH_TYPE_KERNEL_NOLOAD) { images.os.load = images.os.image_start; images.ep += images.os.load; } images.os.start = map_to_sysmem(os_hdr); return 0; }
代码具体执行流程:
- boot_get_kernel函数获取内核镜像在内存地址和大小:
- genimg_get_kernel_addr_fit获取内核真是地址,也就是我们传入的0x30000000参数;
- genimg_get_image解析0x30000000这个地址,如果这个地址位于dataflash storage,将会将内核镜像加载到RAM中;
-
genimg_get_format获取内核镜像头信息,它是在 zImage 之前加上一个长度为0x40的头信息(tag)(也就是说uImage 是一个二进制文件),在头信息内说明了该镜像文件的类型、加载 位置、生成时间、大小等信息;镜像文件的类型有多种:传统格式,FIT格式和安卓格式等;
- 然后初始化images.os.image_start和images.os.image_len,如果是FIT格式,还会初始化images中部分与fit相关的字段;
-
根据返回的头部信息指针,我们去获取到内核想信息并复制给images.os的各个成员,包括内核类型type,内核压缩方式comp,内核是什么操作系统os,内核要装载到内存的哪个位置load,内核是什么体系架构arch,为以后的工作做准备,这里要说明一下,现在内核所在的内存地址是uboot所指定,而内核启动的内存地址不一定在这里,是在laod成员所执行的地址,后面需要把整个镜像拷贝到这里;
- 最后将images.os.load赋值给images.ep,其实就是内核的启动地址了;
2.7 bootm_find_other
2.8 bootm_load_os
2.9 boot_fn
2.10 boot_selected_os
参考文章
[1]七,移植linux-3.19内核
[2][uboot] uboot启动kernel篇(二)——bootm跳转到kernel的流程
[3]linux驱动之uboot启动过程及参数传递
[4]S5PV210-uboot解析(五)-do_bootm函数分析
[5]Linux内核镜像格式
这篇关于Mini440之uboot移植流程之linux内核启动分析(六)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-12如何创建可引导的 ESXi USB 安装介质 (macOS, Linux, Windows)
- 2024-11-08linux的 vi编辑器中搜索关键字有哪些常用的命令和技巧?-icode9专业技术文章分享
- 2024-11-08在 Linux 的 vi 或 vim 编辑器中什么命令可以直接跳到文件的结尾?-icode9专业技术文章分享
- 2024-10-22原生鸿蒙操作系统HarmonyOS NEXT(HarmonyOS 5)正式发布
- 2024-10-18操作系统入门教程:新手必看的基本操作指南
- 2024-10-18初学者必看:操作系统入门全攻略
- 2024-10-17操作系统入门教程:轻松掌握操作系统基础知识
- 2024-09-11Linux部署Scrapy学习:入门级指南
- 2024-09-11Linux部署Scrapy:入门级指南
- 2024-08-21【Linux】分区向左扩容的方法