编译过程
如果需要弄清整个编译过程,那还得好好复习下编译原理。这里只是通过一个小例子讨论大致过程。
准备好一个helloworld的c文件。内容最好简单如下:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
编译命令
$ gcc helloworld.c // 编译
$ ./a.out // 执行
Hello World!
gcc命令其实依次执行了四步操作:
-
预处理(Preprocessing)
- 预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。
- 命令:
gcc -E -I./ helloworld.c -o helloworld.i
或者直接用cpp helloworld.c -I./ -o helloworld.i
- 参数说明:
-
-E
是让编译器在预处理之后就退出,不进行后续编译过程; -
-I
指定头文件目录,这里指定的是我们自定义的头文件目录; -
-o
指定输出文件名。
-
- 经过预处理之后代码体积会大很多。如下是预处理之后的部分内容。
# 1 "helloworld.c" # 1 "<built-in>" 1 # 1 "<built-in>" 3 # 330 "<built-in>" 3 # 1 "<command line>" 1 # 1 "<built-in>" 2 # 1 "helloworld.c" 2 typedef unsigned char __uint8_t; typedef short __int16_t; typedef unsigned short __uint16_t; typedef int __int32_t; typedef unsigned int __uint32_t; typedef long long __int64_t; typedef unsigned long long __uint64_t; typedef struct _opaque_pthread_attr_t __darwin_pthread_attr_t; typedef struct _opaque_pthread_cond_t __darwin_pthread_cond_t; typedef struct _opaque_pthread_condattr_t __darwin_pthread_condattr_t; typedef unsigned long __darwin_pthread_key_t; typedef struct _opaque_pthread_mutex_t __darwin_pthread_mutex_t; typedef struct _opaque_pthread_mutexattr_t __darwin_pthread_mutexattr_t; FILE *fopen(const char * restrict __filename, const char * restrict __mode) __asm("_" "fopen" ); int fprintf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3))); int fputc(int, FILE *); int fputs(const char * restrict, FILE * restrict) __asm("_" "fputs" ); size_t fread(void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream); FILE *freopen(const char * restrict, const char * restrict, FILE * restrict) __asm("_" "freopen" ); int fscanf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__scanf__, 2, 3))); int fseek(FILE *, long, int); int fsetpos(FILE *, const fpos_t *); long ftell(FILE *); size_t fwrite(const void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream) __asm("_" "fwrite" ); int getc(FILE *); int getchar(void); char *gets(char *); void perror(const char *); int printf(const char * restrict, ...) __attribute__((__format__ (__printf__, 1, 2))); int putc(int, FILE *); int putchar(int); int puts(const char *); int remove(const char *); int rename (const char *__old, const char *__new); void rewind(FILE *); int scanf(const char * restrict, ...) __attribute__((__format__ (__scanf__, 1, 2))); void setbuf(FILE * restrict, char * restrict); int setvbuf(FILE * restrict, char * restrict, int, size_t); int sprintf(char * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3))) __attribute__((__availability__(swift, unavailable, message="Use snprintf instead."))); int sscanf(const char * restrict, const char * restrict, ...) __attribute__((__format__ (__scanf__, 2, 3))); FILE *tmpfile(void); __attribute__((__availability__(swift, unavailable, message="Use mkstemp(3) instead."))) __attribute__((deprecated("This function is provided for compatibility reasons only. Due to security concerns inherent in the design of tmpnam(3), it is highly recommended that you use mkstemp(3) instead."))) ... 中间很多内容这里省略 extern int __vsnprintf_chk (char * restrict, size_t, int, size_t, const char * restrict, va_list); # 499 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 2 3 4 # 2 "helloworld.c" 2 int main(){ printf("Hello World!\n"); return 0; }
-
编译(Compilation),
- 这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
- 命令:gcc -S -I./ helloworld.c -o helloworld.s
- 参数:
-S
为了编译之后停止。后面的两个参数含义和预处理的时候一样 - 经过编译之后的内容如下。
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 12 .globl _main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## BB#0: pushq %rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp subq $16, %rsp leaq L_.str(%rip), %rdi movl $0, -4(%rbp) movb $0, %al callq _printf xorl %ecx, %ecx movl %eax, -8(%rbp) ## 4-byte Spill movl %ecx, %eax addq $16, %rsp popq %rbp retq .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "Hello World!\n" .subsections_via_symbols
-
汇编(Assemble),
- 汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。如果有多个文件需要为每一个源文件产生一个目标文件。
- 命令:as helloworld.s -o helloworld.o 或者 gcc -c helloworld.s -o helloworld.o
-
链接(Linking)。
- 链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
- 命令:ld -o helloworld.out helloworld.o
**.o
**.o
。格式其实就是ld(选项)(参数)
参数就是需要连接的目标文件。由于这里没有生成其他目标文件,所以这段不会连接成功的。具体的命令可以看这里ld命令
走完上面的步骤可以得到如下几个文件。
其实我们平时写代码的到得到可执行文件的整个过程可以用下图来概括。
特别需要注意的是宏参数和函数参数的区别,宏参数是进行严格的特换。这如果使用不懂就会出现非常严重的错误。
使用#
参数:宏参数创建字符串
宏定义 | 调用 | 结果 |
---|---|---|
#define TESTPF(x) printf("test "#x" * "#x"=%d\n",(x)*(x)); |
TESTPF(5 + 5) |
test 5 + 5 * 5 + 5=100 |
#define TESTPF(x) printf("test x * x=%d\n",(x)*(x)); |
TESTPF(5 + 5); |
test x * x=100 |
可以看到#
参数的作用就是把字符串中的x也进行了替换。
使用##
参数:预处理粘合剂
##
作用是把两个语言符号组合为单个语言符号。
例子:
#define XNAME(n) x##n
#define PRINT_XN(n) printf("x"#n" = %d \n");
int XNAME(1) = 1;
int XNAME(2) = 2;
PRINT_XN(1);
PRINT_XN(2);
结果:
x1 = 1606416096
x2 = 4352
...
和__VA_ARGS__
:可变宏
这个其实在iOS开发中还是用得挺多的。
例子
#define PR(...) printf(__VA_ARGS__)
PR("DD");
PR("D=%d,F=%d\n",12,22);
结果:
DDD=12,F=22
特别注意。省略号必须在最后一个参数位置。根据这个道理,有些同学可能就能联想到某些语言可变参数的位置为什么一定要在最后把。比如python
文件包含
预处理器发现#include
指令后,会寻找跟在后面的文件,把这个文件中的内容包含到当前文件中。
头文件
OC中有.h和.m文件,这和C里面的.h和.c是同一个道理。所以这里就不多说了。具体看图。
其他预处理指令
-
#undef
取消已定义的宏 -
#if
如果给定条件为真,则编译下面代码 -
#elif
如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 -
#endif
结束一个#if……#else
条件编译块 -
#ifdef
如果宏已经定义,则编译下面代码 -
#ifndef
如果宏没有定义,则编译下面代码 -
#pragma
指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。
上面这些预处理指令,用得比较频繁。大家应该不陌生。还多一些平时用得不多的。
-
#line
指令可以改变编译器用来指出警告和错误信息的文件号和行号。 -
#error
停止编译并显示错误信息
预处理宏
C标准制定的一些预处理宏。
额外补充一个__func__
预定义标识符。这个是C99标准提供的。用于标识当前函数。
上面这些预处理宏经常用于打印一些日志信息。
扩展阅读
C programming Tutorial Introduction to C Programming (for Novices & First-Time Programmers)
C/C++预处理指令#define,#ifdef,#ifndef,#endif…
网友评论