器→工具, 工具软件, 术→技巧, 研发

GCC简明教程

钱魏Way · · 127 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

GCC简介

GCC(GNU Compiler Collection)是由 GNU 项目开发的程序语言编译器。原名为 GNU C Compiler(GNU C 编译器),因为最初只能处理 C 语言。GCC 现在已经能支持多种编程语言,包括 C、C++、Objective-C、Fortran、Ada、Go 等。

GCC 是自由软件基金会(Free Software Foundation)的关键项目,并且是 GNU 操作系统的核心组成部分。GCC 应用非常广泛,大多数 Unix 系统以及其变种都采用 GCC,包括 Linux 和 BSD。另外,GCC 也是跨平台编译器,对于在 Windows 环境开发 Unix 或 Linux 应用程序也非常重要。

GCC 主要有四个部分组成:

  • 预处理器(Preprocessor):处理源代码中的预处理指令,如 #define,#include 等
  • 编译器(Compiler):将预处理后的源代码转换为汇编语言。
  • 汇编器(Assembler):将汇编语言转换为目标文件(二进制代码)。
  • 链接器(Linker):将一个或多个目标文件链接为一个可执行文件。

GCC 不仅是一个编译器,同时也是一个编译器的集合,它还提供了许多高级功能,如内联汇编、各级别的优化、警告控制、错误排查等。

GCC(GNU Compiler Collection)作为一款非常流行的编译器,与其他 C 语言编译器相比,具有一些显著的优点和一些缺点。

优点:

  • 支持多种语言:GCC 不仅支持 C 语言,还支持 C++、Java、Fortran、Ada、Go、Objective-C 等多种编程语言。
  • 跨平台:GCC 是跨平台的,可以在众多操作系统中运行,包括 Unix、Linux、MacOS、Windows 等。
  • 生成高效的代码:GCC 提供多种优化选项,可以生成高效的目标代码。
  • 强大的预处理功能:GCC 的预处理器支持包括宏定义、条件编译、头文件包含等功能。
  • 开源且免费:GCC 是 GNU 项目的一部分,遵循 GPL 协议,可以自由使用、修改和分发。

缺点:

  • 编译速度:GCC 的编译速度相比某些专有的编译器可能会稍慢。
  • 错误提示不够友好:相比某些专有编译器,GCC 的错误和警告信息可能不够明确或者友好,新手可能会觉得难以理解。
  • 对 C++ 的支持:虽然 GCC 也支持 C++,但是在一些特性的支持上,可能不如一些专门针对 C++ 的编译器。

最后,值得注意的是,GCC 的优缺点也在持续变化中,因为 GCC 作为一个活跃的开源项目,一直在进行改进和更新。

gcc 与 g++ 的区别

gcc(GNU Compiler Collection)和 g++ 都是 GNU 编译器的命令行前端,它们的主要区别在于处理 C 和 C++ 代码的方式上。

  • 默认编译语言:gcc 可以被用来编译 C、C++ 和其他语言的代码,但是如果没有明确指定编译语言(例如通过 -xc++),它会假设源文件是 C 语言,而不是 C++。相反,g++ 将默认把源文件当作 C++ 代码来处理。
  • 链接标准库:gcc 和 g++ 在链接阶段的行为也有所不同。默认情况下,gcc 不会链接 C++ 标准库,而 g++ 会。这意味着如果你的代码中使用了 C++ 标准库,例如 <iostream>,那么你需要使用 g++ 或者告诉 gcc 链接 C++ 标准库(通过 -lstdc++ 选项)。

因此,虽然 gcc 和 g++ 都可以用来编译 C 和 C++ 代码,但通常我们使用 gcc 来编译 C 代码,使用 g++ 来编译 C++ 代码,这样可以避免不必要的问题。

请注意,gcc 和 g++ 实际上都是同一个程序。当你调用 g++ 时,它只是以”支持 C++”的方式运行 gcc 程序。这就是为什么 gcc 和 g++ 之间的行为差异可以通过命令行选项来调整的原因。

GCC 的组成

GCC 编译工具链(toolchain),是指以 GCC 编译器为核心的一整套工具。它主要包含以下三部分内容:

  • gcc-core:即 GCC 编译器,用于完成预处理和编译过程,把 C 代码转换成汇编代码。
  • Binutils:除 GCC 编译器外的一系列小工具包括了链接器 ld,汇编器 as、目标文件格式查看器 readelf 等。
  • glibc:包含了主要的 C 语言标准函数库,C 语言中常常使用的打印函数 printf、malloc 函数就在 glibc 库中。

在很多场合下会直接用 GCC 编译器来指代整套 GCC 编译工具链。

Binutils

Binutils 是 GNU 二进制工具集,通常跟 GCC 编译器一起打包安装到系统,它的官方说明网站地址为: https://www.gnu.org/software/binutils/

在进行程序开发的时候通常不会直接调用这些工具,而是在使用 GCC 编译指令的时候由 GCC 编译器间接调用。下面是其中一些常用的工具:

  • as:汇编器,把汇编语言代码转换为机器码(目标文件)。
  • ld:链接器,把编译生成的多个目标文件组织成最终的可执行程序文件。
  • readelf:可用于查看目标文件或可执行程序文件的信息。
  • nm:可用于查看目标文件中出现的符号。
  • objcopy:可用于目标文件格式转换,如 .bin 转换成 .elf、.elf 转换成 .bin 等。
  • objdump:可用于查看目标文件的信息,最主要的作用是反汇编。
  • size:可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等。

系统默认的 Binutils 工具集位于 /usr/bin 目录下,可使用如下命令查看系统中存在的 Binutils 工具集:

glibc 库

  • glibc 库是 GNU 组织为 GNU 系统以及 Linux 系统编写的 C 语言标准函数库,在 Linux 系统下的极大多数 C 语言函数都依赖此函数库运行。
  • 在 Ubuntu 系统下,so.6 是 glibc 的库文件,可直接执行该库文件查看版本,在主机上执行如下命令:/lib/x86_64-linux-gnu/libc.so.6

GCC 的基本命令

GCC 的基本用法是编译源代码文件。以下是一些基本的 GCC 命令:

  • gcc filename.c:用来编译 C 文件生成 out 可执行文件
  • gcc -o output filename.c:编译 C 文件并生成名称为 output 的可执行文件
  • gcc -c filename.c:只预处理和编译,但不链接,生成 obj 文件
  • gcc -E filename.c:只进行预处理,生成预处理后的 C 源文件

GCC 编译流程

文件类型

对于任何给定的输入文件,文件类型决定进行何种编译。GCC 常用的文件类型如表 1 所示:

后缀 描述
.c C 源文件
.C/.cc/.cxx/.cpp C++ 源文件
.h C/C++ 头文件
.i/.ii 经过预处理的 C/C++ 文件
.s/.S 汇编语言源文件
.o/.obj 目标文件
.a/.lib 静态库
.so/.dll 动态库
.out 可执行文件,但可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件。

如果没有给出可执行文件的名字,GCC将生成一个名为a.out的文件。

使用GCC将源代码文件生成可执行文件,需要经过预处理、编译、汇编和链接。

  • 预处理:将源程序(如.c文件)预处理,生成.i文件。
  • 编译:将预处理后的.i文件编译成为汇编语言,生成.s文件。
  • 汇编:将汇编语言文件经过汇编,生成目标文件.o文件。
  • 链接:将各个模块的.o文件链接起来生成一个可执行程序文件。

其中.i文件、.s文件、.o文件是中间文件或临时文件,如果使用GCC一次性完成C语言程序的编译,则这些文件会被删除。

Hello World程序编译

hello.c代码

#include <stdio.h>

int main(){
printf("hello world\n");
return 0;
}

下面就来细讲整个编译的过程。

上图是一个hello的c程序由gcc编译器从源码文件hello.c中读取内容并将其翻译成为一个可执行的对象文件hello的过程。这个过程包含了几个阶段:

预处理过程

预处理(cpp)根据以字符#开头的命令,gcc -E hello.c -o hello.i修改原始的C程序。比如hello.c中的第一行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,插入到程序文本中。结果就得到里另一个C程序,通常是以*.i作为文件扩展名。

可通过执行如下指令获得:

用记事本打开,可查看到如下的内容:

#0 "hello.c"
#0 ""
#0 ""
#1 "/usr/include/stdc-predef.h" 134
#0 "" 2
#1 "hello.c"
#1 "/usr/include/stdio.h" 134
#27 "/usr/include/stdio.h" 34
#1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 134
#33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 34
#1 "/usr/include/features.h" 134
#392 "/usr/include/features.h" 34
#1 "/usr/include/features-time64.h" 134
#20 "/usr/include/features-time64.h" 34
#1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 134
#21 "/usr/include/features-time64.h" 2 34
#1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 134
#19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 34
#1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 134
#20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 34
#22 "/usr/include/features-time64.h" 2 34
#393 "/usr/include/features.h" 2 34
#486 "/usr/include/features.h" 34
#1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 134
#559 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 34
#1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 134
#560 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 34
#1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 134
#561 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 34
#487 "/usr/include/features.h" 2 34
#510 "/usr/include/features.h" 34
#1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 134
#10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 34
#1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 134
#11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 34
#511 "/usr/include/features.h" 2 34
#34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 34
#28 "/usr/include/stdio.h" 2 34typedef unsigned long int __rlim64_t;
typedef unsigned int __id_t;
typedef long int __time_t;
typedef unsigned int __useconds_t;
typedef long int __suseconds_t;
typedef long int __suseconds64_t;

typedef int __daddr_t;
typedef int __key_t;


typedef int __clockid_t;


typedef void *__timer_t;


typedef long int __blksize_t;__attribute__((__nothrow__, __leaf__)) __attribute__((__malloc__)) __attribute__((__malloc__(__builtin_free, 1)));```c
__attribute__((__format__(__scanf__, 1, 0)));


extern int vsscanf(const char *__restrict__ s,
const char *__restrict__ format, __gnuc_va_list __arg)
__attribute__((__nothrow__, __leaf__)) __attribute__((__format__(__scanf__, 2, 0)));__attribute__((__access__(__write_only__, 1)));
#867 "/usr/include/stdio.h" 34
extern void flockfile(FILE *__stream) __attribute__((__nothrow__, __leaf__));hello: file format elf64-x86-64


Disassembly of section .init:

0000000000001000<_init>:
    1000: f3 0f 1e fa           endbr64 
    1004: 48 83 ec 08           sub    $0x8,%rsp
    1008: 48 8b 05 d9 2f 00 00  mov    0x2fd9(%rip),%rax        # 3fe8<__gmon_start__@Base>
    100f: 48 85 c0              test   %rax,%rax
    1012: 74 02                 je     1016<_init+0x16>
    1014: ff d0                 call   *%rax
    1016: 48 83 c4 08           add    $0x8,%rsp
    101a: c3                    ret    

Disassembly of section .plt:

0000000000001020<.plt>:
    1020: ff 35 9a 2f 00 00     push   0x2f9a(%rip)        # 3fc0<_GLOBAL_OFFSET_TABLE_+0x8>
    1026: f2 ff 25 9b 2f 00 00  bnd jmp *0x2f9b(%rip)        # 3fc8<_GLOBAL_OFFSET_TABLE_+0x10>
    102d: 0f 1f 00              nopl   (%rax)
    1030: f3 0f 1e fa           endbr64 
    1034: 68 00 00 00 00        push   $0x0
    1039: f2 e9 e1 ff ff ff     bnd jmp 1020<_init+0x20>
    103f: 90                    nop    

Disassembly of section .plt.got:

0000000000001040<__cxa_finalize@plt>:
    1040: f3 0f 1e fa           endbr64 
    1044: f2 ff 25 ad 2f 00 00  bnd jmp *0x2fad(%rip)        # 3ff8<__cxa_finalize@GLIBC_2.2.5>
    104b: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

Disassembly of section .plt.sec:

0000000000001050:
    1050: f3 0f 1e fa           endbr64 
    1054: f2 ff 25 75 2f 00 00  bnd jmp *0x2f75(%rip)        # 3fd0
    105b: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

0000000000001060<_start>:
    1060: f3 0f 1e fa           endbr64 
    1064: 31 ed                 xor    %ebp,%ebp
    1066: 49 89 d1              mov    %rdx,%r9
    1069: 5e                    pop    %rsi
    106a: 48 89 e2              mov    %rsp,%rdx
    106d: 48 83 e4 f0           and    $0xfffffffffffffff0,%rsp
    1071: 50                    push   %rax
    1072: 54                    push   %rsp
    1073: 45 31 c0              xor    %r8d,%r8d
    1076: 31 c9                 xor    %ecx,%ecx
    1078: 48 8d 3d ca 00 00 00  lea    0xca(%rip),%rdi        # 1149
107f: ff 15 53 2f 00 00 call *0x2f53(%rip) # 3fd8<__libc_start_main@GLIBC_2.34> 1085: f4 hlt 1086: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 108d: 00 00 00 0000000000001090: 1090: 48 8d 3d 79 2f 00 00 lea 0x2f79(%rip),%rdi # 4010<__TMC_END__> 1097: 48 8d 05 72 2f 00 00 lea 0x2f72(%rip),%rax # 4010<__TMC_END__> 109e: 48 39 f8 cmp %rdi,%rax 10a1: 74 15 je 10b8 10a3: 48 8b 05 36 2f 00 00 mov 0x2f36(%rip),%rax # 3fe0<_ITM_deregisterTMCloneTable@Base> 10aa: 48 85 c0 test %rax,%rax 10ad: 74 09 je 10b8 10af: ff e0 jmp *%rax 10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 10b8: c3 ret 10b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 00000000000010c0: 10c0: 48 8d 3d 49 2f 00 00 lea 0x2f49(%rip),%rdi # 4010<__TMC_END__> 10c7: 48 8d 35 42 2f 00 00 lea 0x2f42(%rip),%rsi # 4010<__TMC_END__> 10ce: 48 29 fe sub %rdi,%rsi 10d1: 48 89 f0 mov %rsi,%rax 10d4: 48 c1 ee 3f shr $0x3f,%rsi 10d8: 48 c1 f8 03 sar $0x3,%rax 10dc: 48 01 c6 add %rax,%rsi 10df: 48 d1 fe sar %rsi 10e2: 74 14 je 10f8 10e4: 48 8b 05 05 2f 00 00 mov 0x2f05(%rip),%rax # 3ff0<_ITM_registerTMCloneTable@Base> 10eb: 48 85 c0 test %rax,%rax 10ee: 74 08 je 10f8 10f0: ff e0 jmp *%rax 10f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 10f8: c3 ret 10f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 0000000000001100<__do_global_dtors_aux>: 1100: f3 0f 1e fa endbr64 1104: 80 3d 05 2f 00 00 00 cmpb $0x0,0x2f05(%rip) # 4010<__TMC_END__> 110b: 75 2b jne 1138<__do_global_dtors_aux+0x38> 110d: 55 push %rbp 110e: 48 83 3d e2 2e 00 00 cmpq $0x0,0x2ee2(%rip) # 3ff8<__cxa_finalize@GLIBC_2.2.5> 1115: 00 1116: 48 89 e5 mov %rsp,%rbp 1119: 74 0c je 1127<__do_global_dtors_aux+0x27> 111b: 48 8b 3d e6 2e 00 00 mov 0x2ee6(%rip),%rdi # 4008<__dso_handle> 1122: e8 19 ff ff ff call 1040<__cxa_finalize@plt> 1127: e8 64 ff ff ff call 1090 112c: c6 05 dd 2e 00 00 01 movb $0x1,0x2edd(%rip) # 4010<__TMC_END__> 1133: 5d pop %rbp 1134: c3 ret 1135: 0f 1f 00 nopl (%rax) 1138: c3 ret 1139: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 0000000000001140: 1140: f3 0f 1e fa endbr64 1144: e9 77 ff ff ff jmp 10c0 0000000000001149
: 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004<_IO_stdin_used+0x4> 1158: 48 89 c7 mov %rax,%rdi 115b: e8 f0 fe ff ff call 1050 1160: b8 00 00 00 00 mov $0x0,%eax 1165: 5d pop %rbp 1166: c3 ret Disassembly of section .fini: 0000000000001168<_fini>: 1168: f3 0f 1e fa endbr64 116c: 48 83 ec 08 sub $0x8,%rsp 1170: 48 83 c4 08 add $0x8,%rsp1174: c3ret

经过上面四个过程,我们就可以把一个源代码文件编译成机器能运行的可执行文件。这个可执行文件刚开始是保存在磁盘上,当计算机要运行这个程序的时候,hello就被加载到内存中,接着程序指令被不断复制到寄存器中由CPU来执行,最后把”hello world”从寄存器中打到显示设备上。这就是hello程序整个执行过程。

GCC编译选项

GCC常用选项

GCC(GNU Compiler Collection)是一个开源的编译器集合,它包括了C、C++、Objective-C、Fortran、Ada和Go等语言的编译器。GCC提供了大量的选项,这里列举一些常用的:

  • -o:指定输出文件的名称。例如,gcc -o output input.c将编译input.c并将生成的可执行文件命名为output。
  • -c:只编译但不链接。这用于生成目标文件(.o文件)。例如,gcc -c input.c将生成input.o。
  • -S:只编译但不汇编,生成汇编代码。
  • -E:只进行预处理。
  • -I:指定头文件的搜索路径。例如,gcc -I /path/to/headers input.c将在/path/to/headers目录下搜索头文件。
  • -L和-l:分别用于指定库文件的搜索路径和链接库文件。例如,gcc -L /path/to/libs -l mylib input.c将在/path/to/libs目录下搜索以libmylib开头的库文件。
  • -g:生成调试信息。这对于使用调试器(如GDB)进行调试是必需的。
  • -O:优化代码。-O后面可以跟一个数字来指定优化级别,如-O0(关闭优化)、-O1(开启一些优化,但不会影响编译时间和可调试性)、-O2(开启更多优化,可能会影响编译时间和可调试性)、-O3(开启所有优化)。
  • -W:开启警告。如-Wall开启所有警告,-Wextra开启额外的警告。
  • -std:指定编译的C或C++标准。如-std=c11指定使用C11标准,-std=c++11指定使用C++11标准。
  • -shared:生成共享目标文件。通常在建立共享库时。

以上只是GCC选项中的一部分,更多详细的选项和信息,可以参考GCC的官方文档或者在命令行中使用man gcc命令查看。

GCC多文件编译

如果你的C或C++项目包含多个源文件,你可以使用GCC进行多文件编译。在多文件编译时,通常有两种方法:分步编译和一步编译。

分步编译

首先使用GCC的-c选项将每个源文件编译成目标文件,然后再将所有目标文件链接成一个可执行文件。例如,如果你的项目包含main.c、file1.c和file2.c三个源文件,你可以这样编译:

gcc -c main.c #生成main.o
gcc -c file1.c #生成file1.o
gcc -c file2.c #生成file2.o
gcc -o prog main.o file1.o file2.o #链接目标文件生成可执行文件prog

一步编译

GCC可以一次接收多个源文件作为输入,并自动完成编译和链接的过程。使用这种方法,上面的例子可以简化为一行命令:

gcc -o prog main.c file1.c file2.c

在大型项目中,通常使用Makefile或其他构建工具来自动化编译过程。这些工具可以跟踪文件的依赖关系和修改,只编译修改过的文件和它们的依赖,从而大大减少编译时间。

最后,注意如果你的项目中使用了库,你可能需要使用-I、-L和-l等选项来指定头文件和库文件的位置。

静态库和动态库

在GCC中创建和链接静态库和动态库需要以下步骤:

创建静态库

首先,你需要使用-c选项编译源文件,但不进行链接,生成目标文件(.o文件)。例如:

gcc -c file1.c #生成file1.o
gcc -c file2.c #生成file2.o

然后,使用ar命令创建静态库(.a文件):

ar rcs libmylib.a file1.o file2.o

这里,r表示插入文件(如果库已经存在,则替换库中的同名文件),c表示创建新库,s表示创建目标文件索引。

创建动态库

创建动态库(.so文件)需要使用-shared和-fPIC选项。例如:

gcc -shared -fPIC -o libmylib.so file1.c file2.c

这里,-shared表示创建动态库,-fPIC表示生成位置无关代码(Position Independent Code),这是创建动态库的要求。

链接静态库

链接静态库需要使用-L和-l选项,并且可以加上-static选项以强制链接静态库。例如:

gcc -static -L . -l mylib -o prog main.c

这里,.表示库文件在当前目录下,-l mylib表示链接名为mylib的库(即libmylib.a文件)。

链接动态库

链接动态库也需要使用-L和-l选项。例如:

gcc -L . -l mylib -o prog main.c

注意,运行使用动态库的程序时,系统需要知道动态库的位置。如果动态库不在系统的库搜索路径中,你需要设置LD_LIBRARY_PATH环境变量。例如,如果libmylib.so在当前目录下,你可以这样运行程序:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

./prog

这些只是创建和链接库的基本步骤,实际操作可能会更复杂,例如你可能需要使用Makefile或其他构建工具来管理编译过程,或者使用nm、objdump等工具来查看和调试库文件。

GCC交叉编译

交叉编译是在一个平台(宿主主机)上生成另一个平台(目标主机)上运行的代码。这是嵌入式系统开发中常见的一种做法,因为嵌入式设备的计算能力有限,不适合直接在设备上编译代码。

在GCC中进行交叉编译需要使用交叉编译器。交叉编译器的名称通常包含目标平台的信息,例如arm-linux-gnueabi-gcc是用于ARM平台、使用Linux操作系统和GNUEABI的GCC交叉编译器。

一旦安装了交叉编译器,你就可以像普通GCC一样使用它。例如:

arm-linux-gnueabi-gcc -o prog main.c

这个命令将在宿主主机上编译main.c,并生成ARM平台上的可执行文件prog。

注意,进行交叉编译时,你可能需要为目标平台提供头文件和库文件。这通常涉及到-I、-L和-l等选项。如果你使用的是嵌入式Linux系统,你可能需要使用Buildroot、Yocto Project或其他工具来生成完整的根文件系统(包括库文件和头文件)。

此外,如果你的项目比较复杂,你可能需要使用Makefile或其他构建工具来管理编译过程,并且可能需要配置编译器的选项,例如优化级别、静态链接或动态链接等。

最后,注意在交叉编译后,你需要在目标主机上测试你的程序,确保它在目标平台上正确运行。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注