编译相关内容(自用)

一. gcc相关

  1. gcc -fPIC 生成与位置无关的代码,可以加载到任意位置执行的代码。它适用于编译动态库(shared library),因为动态库需要在多个进程的地址空间中加载和执行。

  2. -shared 用于生成动态库(shared library)

  3. -fpie选项表示在编译阶段生成一个与位置无关的可执行文件(Position Independent Executable)。它使得编译器会为程序中的每个函数生成一个由全局偏移表和程序相对偏移地址相加而确定的全局指针,从而实现了函数位置的可移植性。

  4. pie选项,即使用使用位置独立程序库(Position Independent Executable),这样生成的可执行文件就是一个裸的ELF文件,没有固定加载地址,即可以在任何内存地址位置运行。这样做的好处在于可以提高程序的安全性,防止一些针对固定地址的攻击,如缓冲区溢出等。

  5. -WL,pram1,pram2,pram3 将后面的参数传递给ld,即ld pram1 pram2 pram3

  6. 链接脚本需要添加rel.dyn和.dynsym段定义,重定位过程中,需要根据新的地址修正.rel.dyn和.dynsym段的数据,以确保程序可以正确地运行。

  7. gcc -save-temps 保存过程中间文件

  8. 调试分析选项
    -g:生成调试细腻些,GNU调试器可利用该信息,将调试信息加入到目标文件中
    -pg:编译完成之后,额外产生一个性能分析所需的信息

  9. gcc -static GCC在默认情况下链接的是动态库,使用这个选项会强制程序链接静态库

  10. gcc调用相关系系统调用

execve("/usr/bin/gcc", ["gcc", "main.c"], 0x7ffcfec09f30 /* 56 vars */) = 0 
[pid 14690] execve("/usr/lib/gcc/x86_64-linux-gnu/4.8/cc1", ["/usr/lib/gcc/x86_64-linux-gnu/4."..., "-quiet", "-imultiarch", "x86_64-linux-gnu", "main.c", "-qu
[pid 14691] execve("/usr/bin/as", ["as", "--64", "-o", "/tmp/ccVmeGFC.o", "/tmp/ccBDOm4I.s"], 0x90a330 /* 59 vars */ <unfinished ...>
[pid 14692] execve("/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2", ["/usr/lib/gcc/x86_64-linux-gnu/4."..., "--sysroot=/", "--build-id", "--eh-frame-hdr", "-m", "
[pid 14693] execve("/usr/bin/ld", ["/usr/bin/ld", "--sysroot=/", "--build-id", "--eh-frame-hdr", "-m", "elf_x86_64", "--hash-style=gnu", "--as-needed", "-dynam
  1. gcc 代码优化选项
    代码优化指的是编译器通过分析源代码赵卒尚未达到最优的部分,将其重新组合,改善代码的执行性能 -On(O0,O1,O2,O3,Ofast)

  2. gcc 警告选项
    -w 禁止输出警告消息
    -Werror 将所有警告转换为错误
    -Wall 显示所有的警告信息

  3. gcc -I < DIR> 库依赖选项 ,用来指定库及头文件的路径
    gcc -I /home/include -o Test Test.c

  4. gcc -L< DIR>依赖选项,用来指定库所在路径
    gcc -Test.c -L /home/zxq/lib -lapp -o Test //这里的-L选项表示GCC去连接库文件libapp.so,注意Linux下的库文件在命名时有一个约定,应该以lib三个字母开头,所有的库文件都遵循相同的规范,因此用-L选项指定链接库文件名时可以省去lib三个字母。

  5. gimple向rtl转换:
    在GCC 10.2.0中,实现GIMPLE向RTL(Register Transfer Language)转换的函数是expand_gimple_stmt_1()。该函数位于GCC源代码文件gcc/gimple-low.c中。
    expand_gimple_stmt_1()函数接受一个gimple_stmt_iterator类型的参数,该参数指向一个GIMPLE语句。函数将这个GIMPLE语句转换为等效的RTL表示形式,并返回对应的RTL基本块头部。

    以下是expand_gimple_stmt_1()函数的原型:
    basic_block
    expand_gimple_stmt_1(gimple_stmt_iterator *gsi);
    其中,gsi是一个指向GIMPLE语句迭代器的指针。

    需要注意的是,expand_gimple_stmt_1()函数属于编译器内部的函数,一般不建议用户直接调用。如果想要进行GIMPLE向RTL的转换,可以使用GCC提供的其他API和工具,例如C++ API或GCC插件等。

    在GCC编译器内部,GIMPLE和RTL是两种不同的中间表示(IR)形式。其中,GIMPLE是高层次的、面向表达式的IR,而RTL是低层次的、面向寄存器和指令的IR。因此,在将源代码转换为目标代码时,GCC会首先将GIMPLE表示形式转换为RTL表示形式。

    GIMPLE向RTL的转换过程一般包括以下步骤:

    根据GIMPLE语句生成对应的RTL表达式,GCC会根据每个GIMPLE语句的类型和操作数,生成对应的RTL表达式。例如,对于一个简单的算术运算x = y + z,GCC会生成类似下面这样的RTL表达式:

    (set (reg:SI x)
    
         (plus (reg:SI y)
    
               (reg:SI z)))
    

    将RTL表达式组成基本块

    GCC将所有的RTL表达式组成一个基本块,并构建出整个函数的RTL控制流图。基本块用于表示程序的基本执行单元,它包含一个前导标签和一系列RTL表达式。控制流图用于表示程序中各个基本块之间的控制流关系,例如条件分支、循环等。

    进行寄存器分配和代码优化,在生成完整的RTL表示形式后,GCC会进行寄存器分配和代码优化。这些优化包括指令选择、调度、寄存器分配、常量折叠等,以提高最终生成的目标代码的质量和性能。

    总之,在GCC编译过程中,GIMPLE向RTL的转换是一个复杂而重要的步骤。它将高级别的表达式转换为低级别的指令序列,并在此基础上进行进一步的优化和处理,最终生成高效且正确的目标代码。

  6. 一个头文件都会使用#ifndef、#define和#endif结合使用,以确保头文件只被编译一次,防止重复定义和编译错误

  7. .s与.S文件之间的区别

    .s文件(小写s):这种文件通常是纯粹的汇编源文件,其中所有的汇编代码都以原始形式存在。

    .S文件(大写S):这种文件是预处理过的汇编源文件,其中包括宏定义、条件编译等预处理指令。

    区分.s文件和.S文件的大小写是为了在文件命名时能够明确地表示它们是否需要预处理。不同的编译工具链可能对大小写的要求有所不同,因此在使用时需要注意。一般来说,建议根据需要进行预处理的情况使用大写的.S文件,而不需要预处理的情况使用小写的.s文件。

  8. 查看AST的详细信息:-fdump-tree-original-raw

二、readelf工具

  1. -x -n: 整个节头表的内容

  2. -s :可以看到符号相关的信息

  3. -r :获得重定位信息

  4. -a, --all:显示所有的信息,相当于使用 -h, -l, -S, -r, -u-s 选项。

  5. -h, --file-header:显示文件头部信息,包括文件类型、入口点地址、程序头部表偏移和节头部表偏移等。

  6. -l, --program-headers:显示程序头部表的内容,可以用于查看目标文件中的各个段(段是可执行文件或目标文件中的一个区块,包含各种信息,比如代码、数据、只读数据等)。

  7. -S, --section-headers:显示节头部表的内容,可以用于查看目标文件中的各个节的信息。

  8. -u, --unwind:显示目标文件的展开表(unwind table)信息,这些信息在 C++ 程序中用于异常处理和栈展开。

  9. -I, --histogram:显示目标文件中字符串表的直方图,可以用于查看字符串的使用频率。

  10. -x <number or name>, --hex-dump=<number or name>:显示指定节的十六进制内容。

三、gdb相关

  1. p–打印常见格式控制字符如下:
  • x 按十六进制格式显示变量。

  • d 按十进制格式显示变量。

  • u 按十六进制格式显示无符号整型。

  • o 按八进制格式显示变量。

  • t 按二进制格式显示变量。

  • a 按十六进制格式显示变量。

  • c 按字符格式显示变量。

  • f 按浮点数格式显示变量。

  1. r : 启动调试,binutils调试时需先gdb -tool;r sourcefile

  2. c :用于加载并调试一个核心转储文件(core dump file),核心转存文件是在程序崩溃时生成的,包含了程序运行时内存状态和相关的调试信息。

  3. continue : 跳到下一个断点

  4. b 有三种形式:在函数处、在当前文件行处、在某个行号文件filename:LineNo

  5. clear:清除断点,clear n清除第n个断点

  6. whatis s:可以查看变量的类型

  7. backtrace(bt):用来查看当前调用堆栈;如果想切换到其他堆栈处,可以使用 frame 命令

(gdb) bt
#0  anetListen (err=0x746bb0 <server+560> "", s=10, sa=0x7e34e0, len=16, backlog=511) at anet.c:452
#1  0x0000000000426e35 in _anetTcpServer (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, af=af@entry=10, backlog=511)
    at anet.c:487
#2  0x000000000042793d in anetTcp6Server (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, backlog=511)
    at anet.c:510
#3  0x000000000042b0bf in listenToPort (port=6379, fds=fds@entry=0x746ae4 <server+356>, count=count@entry=0x746b24 <server+420>) at server.c:1728
#4  0x000000000042fa77 in initServer () at server.c:1852
#5  0x0000000000423803 in main (argc=1, argv=0x7fffffffe648) at server.c:3862
(gdb)

上面的例子一共有6层堆栈,最底层是断点所在的 anetListen() 函数,堆栈编号分别是 #0 ~ #5 ,如果想切换到其他堆栈处,可以使用 frame 命令(简写为 f),该命令的使用方法是“frame 堆栈编号(编号不加 #)”。

  1. info :查看信息 i b(查看断点信息)

    info files : 显示被调试文件的详细信息

    info func:显示所有函数名

    info local:显示当前函数中的局部变量信息

    info prog: 显示被调试程序的执行状态

    info var:显示所有全局和静态变量名称

    info thread:显示线程信息

  2. enable,disable,delete命令用恢复、禁用、删除断点

  3. list(l):用于查看断点处代码,l+和-可以查看前面后面的代码

  4. print 和 ptype:用来打印变量和类型,同时可以用来改变变量的值

  5. info和thread组合可以用来调试多线程程序

  6. next(n):next 命令是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置,遇到函数跳过,不进入函数体

  7. step(n):“单步步入”(step into),顾名思义,就是遇到函数调用,进入函数内部,进入函数后,可以用finish跳出函数,reurn可以指定返回值跳出。

  8. until(u):可以指定程序运行到某一行停下来

  9. jump(j).: location 可以是程序的行号或者函数的地址,jump 会让程序执行流跳转到指定位置执行,当然其行为也是不可控制的,例如跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。如果jump 跳转到的位置后续没有断点,那么 GDB 会执行完跳转处的代码会继续执行。

  10. dispaly:监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。

  11. make : 在不退出gdb的情况下执行make工具

  12. shell:在不退出gdb的情况下运行shell命令

  13. kill:结束当前程序的调试

  14. x/nfu

    n是一个正整数,表示显示内存单元的个数
    f代表显示的格式,字符串用s,地址可以用i
    u表示从当前地址向后请求的字节数,默认是4字节,b代表单字节,h代表双字节,w代表四字节,g代表八字节
    
  15. set args 和 show args 命令

    很多程序需要我们传递命令行参数。在 GDB 调试中,正确的做法是在用 GDB 附加程序后,在使用 run 命令之前,使用“set args 参数内容”来设置命令行参数,可以通过 show args 查看命令行参数是否设置成功。

  16. 调试gcc信息,gdb ac,而后run 源程序

  17. 用gdb查看源代码可以用list命令,但是这个不够灵活。可以使用"layout src"命令,或者按Ctrl-X再按A,就会出现一个窗口可以查看源代码。也可以用使用-tui参数,这样进入gdb里面后就能直接打开代码查看窗口

  18. print 或 p:用于打印变量的值,堆栈的值或者内存地址中的内容

  19. set variable:用于设置变量的值

  20. watch xxx:设置监视点,当监视的变量值发生变化时,程序会被暂停下来

  21. disassemble:反汇编指定的函数或代码地址

  22. kill:终止正在调试的进程

  23. 条件断点:根据条件判断

	(gdb) b execute_pass_list if pass == all_ipa_passes
	(gdb) b execute_pass_list if pass == all_passes

四、Makefile

1.基本规则
 $@--目标文件,$^--所有的依赖文件,$<--第一个依赖文件
 反斜杠(\)是换行符的意思

在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

maked的自动推导,main.o自动推导依赖main.c,“.PHONY”表示,clean是个伪目标文件。

objects = main.o kbd.o command.o display.o \
             insert.o search.o files.o utils.o

   edit : $(objects)
           cc -o edit $(objects)

   main.o : defs.h
   kbd.o : defs.h command.h
   command.o : defs.h command.h
   display.o : defs.h buffer.h
   insert.o : defs.h buffer.h
   search.o : defs.h buffer.h
   files.o : defs.h buffer.h command.h
   utils.o : defs.h

   .PHONY : clean
   clean :
           rm edit $(objects)
2.项目结构
[ren@ecs-132384 _promake]$ ls -aR
.:
.  ..  bin  lib  log  Makefile  src

./bin:
.  ..  main.o  TESTpro

./lib:
.  ..  libhello.a

./log:
.  ..

./src:
.  ..  corefunc  interface  main.cpp  Makefile

./src/corefunc:
.  ..  Makefile  usercore.cpp  usercore.h

./src/interface:
.  ..  Makefile  userip.cpp  userip.h
3.各层级Makefile

可以看到实际项目结构有三个Makefile,位于_promake目录中的顶层Makefile,位于src目录中的次层Makefile,以及各个模块文件目录中的底层Makefile。
①顶层Makefile
顶层Makefile主要作用为控制整个make以及make clean的流程,对通用参数做出初始化并传递给子Makefile,递归调用子Makefile,

CUR_DIR = $(abspath $(dir $(MAKEFILE_LIST))) #寻找当前Makefile文件所在目录的绝对路径
ROOT_DIR := $(CUR_DIR)                       #指定项目根路径
LIB_DIR ?= $(ROOT_DIR)/lib                   #指定库文件目录

LIBRARIES := interface corefunc              #指定要生成静态库文件的目录,也是底层Makefile的所在目录(底层Makefile主要用于生成库文件,供次层链接调用,最终生成可执行文件)
SIM := src                                   #递归执行次层Makefile所用的伪目标
CLEAN_COMMAND := -clean                      #clean命令的后缀拼接(可以不用)
CMD_FORCE = FORCE                            #用于强制更新目标
export ROOT_DIR                              #向下一层的Makefile传递的参数,便于子Makefile将生成的.o文件以及静态库文件放入统一指定的目录中
export LIB_DIR
export CMD_FORCE

.PHONY: all  $(LIBRARIES) $(SIM) $(CMD_FORCE)

all: $(SIM) $(LIBRARIES)
#执行到这里时会先去检查依赖$(LIBRARIES)是否为最新,这里的依赖为伪目标,所以会先执行依赖的命令($(MAKE) --directory=$(ROOT_DIR)/src/$@),这个命令会递归到底层模块目录执行底层的Makefile,每个底层的模块目录会生成一个静态库文件(.a)。所有依赖$(LIBRARIES)的命令执行完成后执行目标$(SIM)的命令($(MAKE) --directory=$(ROOT_DIR)/src),这个命令会递归到src目录执行次顶层的Makefile,在次顶层的Makefile中链接底层Makefile生成的静态库文件以及main.o生成可执行文件。
$(SIM): $(LIBRARIES)
        $(MAKE) --directory=$(ROOT_DIR)/src

$(LIBRARIES):
        $(MAKE) --directory=$(ROOT_DIR)/src/$@
clean: $(SIM)$(CLEAN_COMMAND)

$(SIM)$(CLEAN_COMMAND):
        $(MAKE) interface$(CLEAN_COMMAND) --directory=$(ROOT_DIR)/src/interface   #递归到$(ROOT_DIR)/src/interface目录执行make interface-clean
        $(MAKE) corefunc$(CLEAN_COMMAND) --directory=$(ROOT_DIR)/src/corefunc

②次层Makefile
次层makefile的作用是,生成main.o文件并将底层Makefile生成的库文件与main.o链接到一起,生成可执行文件。

PROJ_NAME := TESTpro                            #指定可执行文件名
$(PROJ_NAME):                                   #指定终极目标,与$(PROJ_NAME):$(PROJ_BIN)规则会进行合并执行

$(if $(ROOT_DIR),,$(error "root dir isn't init"))#检查顶层传下来的参数
$(if $(LIB_DIR),,$(error "lib dir isn't init"))
#指定bin目录,用于存放.o文件以及生成的静态库文件
PROJ_OBJ_DIR := $(ROOT_DIR)/bin
#寻找本Makefile的base目录,结果为"."
PROJ_BASE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_LIST)))
#获取base目录中的cpp源文件
SRC_FILE := $(wildcard $(PROJ_BASE_DIR)/*.cpp)
#生成与源文件对应的.o目标文件名
SRC_OBJS := $(patsubst $(PROJ_BASE_DIR)/%.cpp,$(PROJ_OBJ_DIR)/%.o,$(SRC_FILE))
#指定可执行文件的生成位置
PROJ_BIN = $(PROJ_OBJ_DIR)/$(PROJ_NAME)

#指定要链接的库文件
##PROJ LIB##
PROJ_LIB = $(LIB_DIR)/libhello.a  #三方库
PROJ_LIB += $(PROJ_OBJ_DIR)/libinterface.a  #底层模块生成的静态库文件
PROJ_LIB += $(PROJ_OBJ_DIR)/libcorefunc.a

#添加FLAG,包括警告检查,头文件目录以及库文件目录,需要链接的库名字
##PROJ FLAG##
PROJ_FLAG = -Wall\
            -Wextra\
            -Wshadow\
            -Wpointer-arith\
            -Wcast-qual
PROJ_FLAG += -I$(ROOT_DIR)/src/interface
PROJ_FLAG += -I$(ROOT_DIR)/src/corefunc
PROJ_FLAG += -L$(LIB_DIR)
PROJ_FLAG += -L$(PROJ_OBJ_DIR)

PROJ_FLAG += -lhello -linterface -lcorefunc

.PHONY: $(PROJ_NAME) FORCE

#当一个伪目标(例如FORCE)没有依赖,下方没有可执行的命令,则当其作为其他目标的依赖时,每次FORCE都会被认为是更新的,以此强制目标进行更新
#由于在引入的build.mk文件中往往会存在伪目标,所以在最前方需要写上终极目标$(PROJ_NAME)
FORCE:

$(PROJ_NAME):$(PROJ_BIN)
#由于可执行文件往往是存在的,所以需要加FORCE,保证每次都能强制生成
$(PROJ_BIN): $(PROJ_LIB) $(SRC_OBJS) FORCE
        g++ -o $@ $(SRC_OBJS) $(PROJ_LIB) $(PROJ_FLAG)
$(PROJ_LIB):

$(SRC_OBJS):$(SRC_FILE)
        g++ -o $@ -c $^ -std=c++11 $(PROJ_FLAG)
$(PROJ_NAME)-clean:
        -rm -rf $(PROJ_BIN) $(SRC_OBJS)

③底层Makefile(interface目录下的Makefile为例,其他模块下的类似)
底层Makefile主要用于将各个模块下的代码生成静态库文件(.a),部分命令同次层Makefile,不再赘述

PROJ_NAME := interface
$(PROJ_NAME):

#check root dir#
$(if $(ROOT_DIR),,$(error "root dir isn't init"))
$(if $(LIB_DIR),,$(error "lib dir isn't init"))

PROJ_BIN_DIR = $(ROOT_DIR)/bin
PROJ_BASE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_LIST)))
SRC_FILE := $(wildcard $(PROJ_BASE_DIR)/*cpp)
SRC_OBJ := $(patsubst $(PROJ_BASE_DIR)/%.cpp,$(PROJ_BIN_DIR)/%.o,$(SRC_FILE))
AR = ar -crv                 #静态库生成命令
PROJ_ST_LIB := $(PROJ_BIN_DIR)/lib$(PROJ_NAME).a   #最终生成的静态库名字

PROJ_CXXFLAG = -I$(ROOT_DIR)/src/interface
PROJ_CXXFLAG += -I$(ROOT_DIR)/src/corefunc


.PHONY: $(PROJ_NAME) $(PROJ_NAME)-clean FORCE

FORCE:

$(PROJ_NAME):$(PROJ_ST_LIB)

#由于静态库文件在每次编译时往往是存在的,所以需要强制生成
$(PROJ_ST_LIB):$(SRC_OBJ) FORCE
        $(AR) -o $@ $(SRC_OBJ)

$(SRC_OBJ):$(SRC_FILE)
        g++ -c $^ -std=c++11 $(PROJ_CXXFLAG) -o $@

#这个-clean就是在顶层Makefile中加-clean后缀的原因,可以都不加。
$(PROJ_NAME)-clean:
        -rm -rf $(SRC_OBJ) $(PROJ_ST_LIB)  #清除本Makefile生成的文件,-rm的意思

五、buildroot

  1. 编译有debug信息的gcc
    因为只编gcc,可以只在buildroot目录下进行,但是需要主要将sources目录下Makefile中关联的依赖包提前解压(gcc,gdb,binutils),基本步骤如下:
    make arceb_nps_soc_defconfig生成根目录下的.config文件 将BR2_JLEVEL设置为72(线程) BR2_BINUTILS_EXTRA_CONFIG_OPTIONS设置为"--enable-debug CFLAGS='-O0 -g3' CXXCFLAGS='-O0 -g3'" 最后make host-binutils即可 注:在编译前可以具体看下make menuconfig图像化界面查看相关选项。

  2. buildroot编译toolchain依赖包

make graph-depends 生成目录为output/graph/

buildroot-nps-2016.11-h3csemi.tar.bz2

m4-1.4.17.tar.xz
gmp-6.1.1.tar.xz
mpfr-3.1.5.tar.xz
mpc-1.0.3.tar.gz
flex-2.5.37.tar.gz
binutils-arc-2015.06-h3csemi.tar.gz
gcc-arc-2015.06-h3csemi.tar.gz
uClibc-arc-2015.06-h3csemi.tar.xz
bison-3.0.4.tar.xz
linux-4.8-arc-h3csemi.tar.xz
gdb-arc-2014.08-h3csemi.tar.gz
gdb-arc-2015.06-h3csemi.tar.gz
ncurses-5.9.tar.gz
pkgconf-0.9.12.tar.bz2
expat-2.2.0.tar.bz2

六、gcc原生工具使用

  1. ar x xx.a //将.o文件从.a静态库中提取
  2. ar cru xx.a *.o //将.o文件转为.a静态库
    c表示创建一个新的库文件,如果xx.a不存在,则创建一个新的库文件;如果已存在,则替换掉其中的所有目标文件。
    r表示将指定的目标文件添加到库文件中,这里 *.o 表示当前目录下所有以.o为后缀的文件。如果其中的某个目标文件已经存在于库文件中,那么替换掉原有的目标文件。
    u 表示更新库文件,只有当某个目标文件的修改时间比库文件中已存在的同名目标文件的修改时间要新,才会将这个目标文件添加进库文件。
  3. ar rcs xx.a *.o
    s 表示在写入文件之前对该文件进行排序,这样可以加快链接的速度。
  4. ranlib //xx.a 这个命令用于为静态库文件 xx.a 创建一个新的索引。索引包含了静态库文件中所有目标文件的名字以及相关信息。这个索引可以加快链接器在静态库中查找目标文件的速度。
  5. ar t xx.a 查看xx.a静态库中包含的.o文件
  • 66
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值