zoukankan      html  css  js  c++  java
  • 嵌入式Linux之uboot源码make编译逆向分析

    一、make编译逆向分析之prepare

    在上一篇文章中分析Makefile文件依赖关系时采用自顶向下的方法,从顶层目标开始到最原始的依赖结束。

    此处我们采用自下而上的方式,从最原始的依赖开始,一步一步的执行命令生成目标。

    完整的prepare的目标依赖:

    依次从最右边的依赖说起;

    1.1  .config(scripts/kconfig/conf生成的文件)

    .config在执行make smdk2410_defconfig配置时生成,文件scripts/kconfig/Makefile中定义了所有匹配%config的目标:

    %_defconfig: $(obj)/conf
        $(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

    展开为:

    smdk2410_defconfig: scripts/kconfig/conf
        $(Q)scripts/kconfig/conf  --defconfig=arch/../configs/smdk2410_defconfig Kconfig

    scripts/kconfig/conf会从根目录开始读取Kconfig,输出到根目录下的.config中:

    • include/generated/autoconf.h
    • include/config/auto.conf.cmd
    • include/config/tristate.conf
    • include/config/auto.conf

    以上4个文件在执行make编译命令的开始会检查%.conf的依赖规则:

    include/config/%.conf: $(KCONFIG_CONFIG) include/config/auto.conf.cmd
        $(Q)$(MAKE) -f $(srctree)/Makefile silentoldconfig
        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf || 
            { rm -f include/config/auto.conf; false; }
        $(Q)touch include/config/auto.conf

    调用make -f ./Makefile silentoldconfig的最终结果是执行scripts/kconfig/Makefile中的规则:

    silentoldconfig: $(obj)/conf
        $(Q)mkdir -p include/config include/generated
        $< $(silent) --$@ $(Kconfig)

    这个规则展开为:

    silentoldconfig: scripts/kconfig/conf
        mkdir -p include/config include/generated
        scripts/kconfig/conf --silentoldconfig Kconfig

    scripts/kconfig/conf会从根目录开始读取Kconfig,同时检查并更新配置阶段生成的.config文件,再把最终结果输出到以上的4个文件中。

    所生成的4个文件中,include/config/auto.conf依赖于include/config/auto.conf.cmd,但是这里的依赖文件include/config/auto.conf.cmd文件并非由fixdep生成,而是直接由conf工具生成,算是*.cmd文件生成的特例。 

    scripts/kconfig/conf生成了图中右侧的依赖:include/config/auto.conf,$(KCONIFG_CONFIG)/.config和include/config/auto.conf.cmd。

    1.2 目标include/config/auto.conf的规则

    在生成include/config/auto.conf的规则中:

    # If .config is newer than include/config/auto.conf, someone tinkered
    # with it and forgot to run make oldconfig.
    # if auto.conf.cmd is missing then we are probably in a cleaned tree so
    # we execute the config step to be sure to catch updated Kconfig files
    include/config/%.conf: $(KCONFIG_CONFIG) include/config/auto.conf.cmd
        $(Q)$(MAKE) -f $(srctree)/Makefile silentoldconfig
        @# If the following part fails, include/config/auto.conf should be
        @# deleted so "make silentoldconfig" will be re-run on the next build.
        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf || 
            { rm -f include/config/auto.conf; false; }
        @# include/config.h has been updated after "make silentoldconfig".
        @# We need to touch include/config/auto.conf so it gets newer
        @# than include/config.h.
        @# Otherwise, 'make silentoldconfig' would be invoked twice.
        $(Q)touch include/config/auto.conf

    除了执行$(MAKE) -f  $(srctree)/Makefile silentoldconfig外,还执行$(MAKE) -f  $(srctree)/scripts/Makefile.autoconf 在scripts/Makefile.autoconf的头部是这样的:

    # This helper makefile is used for creating
    #  - symbolic links (arch/$ARCH/include/asm/arch
    #  - include/autoconf.mk, {spl,tpl}/include/autoconf.mk
    #  - include/config.h
    #
    # When our migration to Kconfig is done
    # (= When we move all CONFIGs from header files to Kconfig)
    # this makefile can be deleted.
    #
    # SPDX-License-Identifier:  GPL-2.0
    #
    
    __all: include/autoconf.mk include/autoconf.mk.dep
    
    ifeq ($(shell grep -q '^CONFIG_SPL=y' include/config/auto.conf 2>/dev/null && echo y),y)
    __all: spl/include/autoconf.mk
    endif
    
    ifeq ($(shell grep -q '^CONFIG_TPL=y' include/config/auto.conf 2>/dev/null && echo y),y)
    __all: tpl/include/autoconf.mk
    endif

    此处没有设置CONFIG_SPL=y或CONFIG_TPL=y,所以整个makefile的__all的依赖有:

    • include/autoconf.mk
    • include/autoconf.mk.dep

    然而include/autoconf.mk还要进一步依赖于config.h。

    (1) include/config.h的规则

    所有的autoconf.mk都依赖于include/config.h:

    include/autoconf.mk include/autoconf.mk.dep 
        spl/include/autoconf.mk tpl/include/autoconf.mk: include/config.h

    实际上include/config.h由宏filechk_config_h生成:

    # include/config.h
    # Prior to Kconfig, it was generated by mkconfig. Now it is created here.
    define filechk_config_h
        (echo "/* Automatically generated - do not edit */";        
        for i in $$(echo $(CONFIG_SYS_EXTRA_OPTIONS) | sed 's/,/ /g'); do 
            echo #define CONFIG_$$i                
            | sed '/=/ {s/=/    /;q; } ; { s/$$/    1/; }'; 
        done;                               
        echo #define CONFIG_BOARDDIR board/$(if $(VENDOR),$(VENDOR)/)$(BOARD);
        echo #include <config_defaults.h>;               
        echo #include <config_uncmd_spl.h>;              
        echo #include <configs/$(CONFIG_SYS_CONFIG_NAME).h>;     
        echo #include <asm/config.h>;                
        echo #include <config_fallbacks.h>;)
    endef
    
    include/config.h: scripts/Makefile.autoconf create_symlink FORCE
        $(call filechk,config_h)

    最终生成的include/config.h也比较简单,不妨看看:

    /* Automatically generated - do not edit */
    #define CONFIG_BOARDDIR board/samsung/smdk2410
    #include <config_defaults.h>
    #include <config_uncmd_spl.h>
    #include <configs/smdk2410.h>
    #include <asm/config.h>
    #include <config_fallbacks.h>

    生成config.h之前,还要应用create_symlink生成相应的符号链接。

    (2) create_symlink的规则

    # symbolic links
    # If arch/$(ARCH)/mach-$(SOC)/include/mach exists,
    # make a symbolic link to that directory.
    # Otherwise, create a symbolic link to arch/$(ARCH)/include/asm/arch-$(SOC).
    PHONY += create_symlink
    create_symlink:
    ifdef CONFIG_CREATE_ARCH_SYMLINK
    ifneq ($(KBUILD_SRC),)
        $(Q)mkdir -p include/asm
        $(Q)if [ -d $(KBUILD_SRC)/arch/$(ARCH)/mach-$(SOC)/include/mach ]; then 
            dest=arch/$(ARCH)/mach-$(SOC)/include/mach;         
        else                                    
            dest=arch/$(ARCH)/include/asm/arch-$(if $(SOC),$(SOC),$(CPU));  
        fi;                                 
        ln -fsn $(KBUILD_SRC)/$$dest include/asm/arch
    else
        $(Q)if [ -d arch/$(ARCH)/mach-$(SOC)/include/mach ]; then   
            dest=../../mach-$(SOC)/include/mach;            
        else                                
            dest=arch-$(if $(SOC),$(SOC),$(CPU));           
        fi;                             
        ln -fsn $$dest arch/$(ARCH)/include/asm/arch
    endif
    endif

    注释已经很好解释了create_symlink的行为:

    • 如果arch/$(ARCH)/mach-$(SOC)/include/mach存在,则生成符号链接:
    arch/$(ARCH)/include/asm/arch --> arch/$(ARCH)/mach-$(SOC)
    • 否则生成符号链接:
    arch/$(ARCH)/include/asm/arch -->arch/$(ARCH)/include/asm/arch-$(SOC) 

    对基于arm v9架构的s3c24x0芯片,arch/arm/mach-s3c2440文件夹不存在,所以生成链接:

    arch/arm/include/asm/arch --> arch/arm/include/asm/arch-s3c24x0

    简单说来,create_symlink就是将芯片arch/arm/include/asm/arch-s3c24x0链接到跟芯片名字无关的arch/arm/include/asm下。 

    (3) include/autoconf.mk的规则

    # We are migrating from board headers to Kconfig little by little.
    # In the interim, we use both of
    #  - include/config/auto.conf (generated by Kconfig)
    #  - include/autoconf.mk      (used in the U-Boot conventional configuration)
    # The following rule creates autoconf.mk
    # include/config/auto.conf is grepped in order to avoid duplication of the
    # same CONFIG macros
    quiet_cmd_autoconf = GEN     $@
          cmd_autoconf = 
        $(CPP) $(c_flags) $2 -DDO_DEPS_ONLY -dM $(srctree)/include/common.h > $@.tmp && { 
            sed -n -f $(srctree)/tools/scripts/define2mk.sed $@.tmp |       
            while read line; do                         
                if [ -n "${KCONFIG_IGNORE_DUPLICATES}" ] ||         
                   ! grep -q "$${line%=*}=" include/config/auto.conf; then  
                    echo "$$line";                      
                fi                              
            done > $@;                              
            rm $@.tmp;                              
        } || {                                      
            rm $@.tmp; false;                           
        }
    
    include/autoconf.mk: FORCE
        $(call cmd,autoconf)

    从cmd_autoconf来看,这里会根据include/common.h的依赖,然后调用tools/scripts/define2mk.sed,并合并之前生成的include/config/auto.conf生成最终的autoconf.mk。

    (4) include/autoconf.mk.dep的规则

    quiet_cmd_autoconf_dep = GEN     $@
          cmd_autoconf_dep = $(CC) -x c -DDO_DEPS_ONLY -M -MP $(c_flags) 
        -MQ include/config/auto.conf $(srctree)/include/common.h > $@ || {  
            rm $@; false;                           
        }
    include/autoconf.mk.dep: FORCE
        $(call cmd,autoconf_dep)

    这个规则比较简单,由于autoconf.mk由common.h和auto.conf而来,因此直接处理这两个文件的依赖并合并到autoconf.mk.dep中。

    1.3 include/config/uboot.release

    define filechk_uboot.release
        echo "$(UBOOTVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))"
    endef
    
    # Store (new) UBOOTRELEASE string in include/config/uboot.release
    include/config/uboot.release: include/config/auto.conf FORCE
        $(call filechk,uboot.release)

    命令$(call filechk,uboot.release)展开后就是调用宏filechk_uboot.release,最终将字符串2016.09写入include/config/uboot.release中。

    1.4 timestamp.h和version.h的规则

    ersion_h := include/generated/version_autogenerated.h
    timestamp_h := include/generated/timestamp_autogenerated.h
    
    ...
    
    # Generate some files
    # ---------------------------------------------------------------------------
    
    define filechk_version.h
        (echo #define PLAIN_VERSION "$(UBOOTRELEASE)"; 
        echo #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION; 
        echo #define CC_VERSION_STRING "$$(LC_ALL=C $(CC) --version | head -n 1)"; 
        echo #define LD_VERSION_STRING "$$(LC_ALL=C $(LD) --version | head -n 1)"; )
    endef
    
    # The SOURCE_DATE_EPOCH mechanism requires a date that behaves like GNU date.
    # The BSD date on the other hand behaves different and would produce errors
    # with the misused '-d' switch.  Respect that and search a working date with
    # well known pre- and suffixes for the GNU variant of date.
    define filechk_timestamp.h
        (if test -n "$${SOURCE_DATE_EPOCH}"; then 
            SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; 
            DATE=""; 
            for date in gdate date.gnu date; do 
                $${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; 
            done; 
            if test -n "$${DATE}"; then 
                LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; 
                LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; 
                LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; 
                LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; 
            else 
                return 42; 
            fi; 
        else 
            LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; 
            LC_ALL=C date +'#define U_BOOT_TIME "%T"'; 
            LC_ALL=C date +'#define U_BOOT_TZ "%z"'; 
            LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; 
        fi)
    endef
    
    $(version_h): include/config/uboot.release FORCE
        $(call filechk,version.h)
    
    $(timestamp_h): $(srctree)/Makefile FORCE
        $(call filechk,timestamp.h)

    (1) include/generated/version_autogenerated.h

    根据include/config/uboot.release文件,规则调用filechk_version.h宏生成版本相关字符串文件include/generated/version_autogenerated.h,如下:

    #define PLAIN_VERSION "2016.05"
    #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
    #define CC_VERSION_STRING "arm-none-linux-gnueabi-gcc (Sourcery G++ Lite 2008q3-72) 4.3.2"
    #define LD_VERSION_STRING "GNU ld (Sourcery G++ Lite 2008q3-72) 2.18.50.20080215

    (2) include/generated/timestamp_autogenerated.h

    调用宏filechk_timestamp.h生成编译的时间戳文件,如下:

    #define U_BOOT_DATE "Jun 07 2021"
    #define U_BOOT_TIME "01:58:59"
    #define U_BOOT_TZ "+0800"
    #define U_BOOT_DMI_DATE "06/07/2021"

    1.5 outputmakfile的规则

    PHONY += outputmakefile
    # outputmakefile generates a Makefile in the output directory, if using a
    # separate output directory. This allows convenient use of make in the
    # output directory.
    outputmakefile:
    ifneq ($(KBUILD_SRC),)
        $(Q)ln -fsn $(srctree) source
        $(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile 
            $(srctree) $(objtree) $(VERSION) $(PATCHLEVEL)
    endif
    • 如果编译没有设置O,即输出和代码都在同一个目录下,则outputmakefile的规则什么都不做;
    • 如果编译指定了输出目录O,则调用scripts/mkmakefile在O选项指定的目录下生成一个简单的makefile

    1.6 scripts_basic的规则

    # Basic helpers built in scripts/
    PHONY += scripts_basic
    scripts_basic:
        $(Q)$(MAKE) $(build)=scripts/basic
        $(Q)rm -f .tmp_quiet_recordmcount

    scripts_basic的执行结果就是编译生成scripts/basic/fixdep工具,该工具是u-boot编译系统中最常用的工具,用于在编译过程中修正每一个生成文件的依赖关系。

    1.7 parepare0的规则

    prepare0: archprepare FORCE
        $(Q)$(MAKE) $(build)=.

    展开后为:

    prepare0: archprepare FORCE
        make -f ./scripts/Makefile.build obj=.

    编译时,命令make -f ./scripts/Makefile.build obj=.不会生成任何目标。

    1.8 prepare系列目标总结

    prepare阶段主要做了以下工作:

    • scripts_basic规则生成fixdep工具,用于对整个系统生成目标文件相应依赖文件的更新;
    • 配置阶段,scripts/kconfig/conf根据传入的指定配置文件在根目录下生成.config文件;
    • 编译阶段,scripts/kconfig/conf读取配置阶段生成的.config,并检查最新配置生成以下文件:
      • include/generated/autoconf.h
      • include/config/auto.conf.cmd
      • include/config/tristate.conf
      • include/config/auto.conf
    • 调用宏filechk_config_h生成include/config.h文件;
    • 调用命令cmd_autoconf_dep生成autoconf.mk和autoconf.mk.cmd文件;
    • 调用宏filechk_uboot.release生成include/config/uboot.release文件;
    • 调用宏filechk_version.h生成include/generated/version_autogenerated.h文件;
    • 调用宏filechk_timestamp.h生成include/generated/timestamp_autogenerated.h文件;
    • 调用宏create_symlink就是将芯片arch/$(ARCH)/include/asm/arch-$(SOC)链接到跟芯片名字无关的arch/$(ARCH)/include/asm下。

    二、make编译逆向分析之u-boot

    完整的u-boot的目标依赖:

    从图上可见,除了prepare依赖外,u-boot还依赖于文件$(head-y),$(libs-y)和$(LDSCRIPT),即依赖于:

    • 启动文件arch/arm/cpu/$(CPU)/start.o ;
    • 各个目录下的build-in.o ;
    • 链接脚本文件arch/arm/cpu/u-boot.lds;

    2.1 启动文件start.o

    $(head-y)在arch/arm/Makefile中被直接指定:

    head-y := arch/arm/cpu/$(CPU)/start.o

    在顶层makefile中被指定给变量u-boot-init:

    u-boot-init := $(head-y)

    2.2 各目录下的build-in.o

    $(libs-y)在顶层的makefile中被指定为各个子目录下的build-in.o的集合:

    libs-y += lib/
    ...
    libs-y += fs/
    libs-y += net/
    libs-y += disk/
    libs-y += drivers/
    ...
    
    libs-y += $(if $(BOARDDIR),board/$(BOARDDIR)/)
    
    libs-y := $(sort $(libs-y))
    
    ...
    
    libs-y      := $(patsubst %/, %/built-in.o, $(libs-y))
    
    ...
    u-boot-main := $(libs-y)

    以上脚本中,先将$(libs-y)设置为各子目录的集合,最后调用patsubst函数将$(libs-y)设置为这些目录下的built-in.o文件的集合,最后赋值给变量u-boot-main作为链接的主体文件。

    各目录下的built-in.o是如何生成的呢?

    以drivers/mmc/built-in.o为例,先查看生成的依赖文件drivers/mmc/.built-in.o.cmd:

    cmd_drivers/mmc/built-in.o :=  rm -f drivers/mmc/built-in.o; arm-linux-ar rcs drivers/mmc/built-in.o

    生成built-in.o的规则在scripts/Makefile.build中定义:

    #
    # Rule to compile a set of .o files into one .o file
    #
    ifdef builtin-target
    quiet_cmd_link_o_target = LD      $@
    # If the list of objects to link is empty, just create an empty built-in.o
    cmd_link_o_target = $(if $(strip $(obj-y)),
                  $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) 
                  $(cmd_secanalysis),
                  rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)
    
    $(builtin-target): $(obj-y) FORCE
        $(call if_changed,link_o_target)
    
    targets += $(builtin-target)
    endif # builtin-target

    2.3 链接脚本u-boot.lds

    链接脚本的规则如下:

    quiet_cmd_cpp_lds = LDS     $@
    cmd_cpp_lds = $(CPP) -Wp,-MD,$(depfile) $(cpp_flags) $(LDPPFLAGS) 
            -D__ASSEMBLY__ -x assembler-with-cpp -P -o $@ $<
    
    u-boot.lds: $(LDSCRIPT) prepare FORCE
        $(call if_changed_dep,cpp_lds)

    2.4 生成u-boot规则

    顶层Makefile中定义了生成u-boot文件的规则:

    # Rule to link u-boot
    # May be overridden by arch/$(ARCH)/config.mk
    quiet_cmd_u-boot__ ?= LD      $@
          cmd_u-boot__ ?= $(LD) $(LDFLAGS) $(LDFLAGS_u-boot) -o $@ 
          -T u-boot.lds $(u-boot-init)                             
          --start-group $(u-boot-main) --end-group                 
          $(PLATFORM_LIBS) -Map u-boot.map
    
    ...
    
    u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
        $(call if_changed,u-boot__)
    ...

    u-boot文件的生成很简单,调用ld命令,将$(u-boot-init)和$(u-boot-main)指定的一系列文件通过脚本u-boot.lds连接起来。

    生成了u-boot文件后,后续就是针对u-boot文件的各种处理了。

    三、make编译逆向分析之 顶层目标依赖

    顶层目标依赖:

     

    显然,在生成了u-boot的基础上,进一步生成所需要的各种目标文件:

    3.1 u-boot.srec

    # Normally we fill empty space with 0xff
    quiet_cmd_objcopy = OBJCOPY $@
    cmd_objcopy = $(OBJCOPY) --gap-fill=0xff $(OBJCOPYFLAGS) 
        $(OBJCOPYFLAGS_$(@F)) $< $@
    ...
    OBJCOPYFLAGS_u-boot.hex := -O ihex
    
    OBJCOPYFLAGS_u-boot.srec := -O srec
    
    u-boot.hex u-boot.srec: u-boot FORCE
        $(call if_changed,objcopy)

    调用objcopy命令,通过-O ihex或-O srec指定生成u-boot.hex或u-boot.srec格式文件。

    3.2 u-boot.sym

    quiet_cmd_sym ?= SYM     $@
          cmd_sym ?= $(OBJDUMP) -t $< > $@
    u-boot.sym: u-boot FORCE
        $(call if_changed,sym)

    调用$(OBJDUMP)命令生成符号表文件u-boot.sym。

    3.3 System.map

    SYSTEM_MAP = 
            $(NM) $1 | 
            grep -v '(compiled)|(.o$$)|( [aUw] )|(..ng$$)|(LASH[RL]DI)' | 
            LC_ALL=C sort
    System.map: u-boot
            @$(call SYSTEM_MAP,$<) > $@

    调用$(NM)命令打印u-boot文件的符号表,并用grep -v处理后得到System.map文件,里面包含了最终使用到的各个符号的位置信息。

    3.4 u-boot.bin和u-boot-nodtb.bin

    PHONY += dtbs
    dtbs: dts/dt.dtb
        @:
    dts/dt.dtb: checkdtc u-boot
        $(Q)$(MAKE) $(build)=dts dtbs
    
    quiet_cmd_copy = COPY    $@
          cmd_copy = cp $< $@
    
    ifeq ($(CONFIG_OF_SEPARATE),y)
    u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
        $(call if_changed,cat)
    
    u-boot.bin: u-boot-dtb.bin FORCE
        $(call if_changed,copy)
    else
    u-boot.bin: u-boot-nodtb.bin FORCE
        $(call if_changed,copy)
    endif

    由于这里没有使用device tree设置,即编译没有定义CONFIG_OF_SEPARATE,因此u-boot.bin和u-boot-nodtb.bin是一样的。

    至于生成u-boot-nodtb.bin的规则:

    u-boot-nodtb.bin: u-boot FORCE
        $(call if_changed,objcopy)
        $(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
        $(BOARD_SIZE_CHECK)

    显然,u-boot-nodtb.bin是u-boot文件通过objcopy得到。

    3.5 u-boot.cfg

    u-boot.cfg中包含了所有用到的宏定义,其生成规则如下:

    # Create a file containing the configuration options the image was built with
    quiet_cmd_cpp_cfg = CFG     $@
    cmd_cpp_cfg = $(CPP) -Wp,-MD,$(depfile) $(cpp_flags) $(LDPPFLAGS) -ansi 
        -DDO_DEPS_ONLY -D__ASSEMBLY__ -x assembler-with-cpp -P -dM -E -o $@ $<
    ...
    u-boot.cfg: include/config.h FORCE
        $(call if_changed,cpp_cfg)

    因此,阅读源码时如果不确定某个宏的值,可以检查u-boot.cfg文件。 自此,生成了所有的目标文件,完成了整个编译过程的分析。

  • 相关阅读:
    Android中 requestCode与resultCode的区别与用法
    Activity与Fragment之间的通信
    关于解决 从相册中选择照片后无法剪切图片以及无法加载图片的问题
    Codeforces Round #313 (Div. 2) C. Geralds Hexagon
    HDU 2669 Romantic
    HDU 1405 The Last Practice
    HDU Wolf and Rabbit
    LightOJ 1104 Birthday Paradox
    11181
    Tr A
  • 原文地址:https://www.cnblogs.com/zyly/p/14860850.html
Copyright © 2011-2022 走看看