操作系统—GCC与编译全流程

文章目录

  • GCC与编译全流程
    • 1.GCC是什么?
    • 2.编译全流程
      • (1).GCC到底做了哪些事情?
      • (2).预处理
        • I.预处理会做什么
        • II.预处理器主要包含什么?
        • III.宏的一些魔法
      • (3).编译
        • I.基本流程
        • II.编译优化
        • III.一点例子
      • (4).汇编
      • (5).链接
      • (6).说到这里,为什么我们要用gcc呢?
    • 3.还有别的选择吗?
    • 4.杂谈
      • (1).自己编译一个GCC
      • (2).构建系统和Makefile
    • 总结

GCC与编译全流程

1.GCC是什么?

  GCC全称GNU Compiler Collections,也就是FSF(自由软件基金会)的GNU推出的一个编译器集合,很有意思的是,GNU早期是为了实现一个与Unix一致,但是完全自由的操作系统而生的,所以GNU采取了一个递归命名,它的全称为GNU is Not Unix

  实际上,目前的GCC已经成为了C/C++编译器领域的一霸(还有基于LLVM的Clang和微软的MSVC),不过事实上GCC并不是GNU C Compiler,这个编译器集合包括的能够编译的语言远比我们想的多:C, C++, Objective-C, Fortran, Ada, Go, and D(来自GCC官网的介绍),不过现在主要还是拿它来编译C/C++。

  所以背景已经了解完了,GCC是一个编译器集合,它可以帮助我们完成编译一个C语言程序的全部流程,如果只是为了得到一个可执行文件,我们只需要简单地输入这样一条指令:

gcc hello.c -o hello

  这里指定了hello.c作为需要编译的源代码文件,然后用“-o + 名字”指定最终编译出的可执行文件的名字,所以接下来我们就应该分析一下整个编译流程了。

2.编译全流程

(1).GCC到底做了哪些事情?

  我当然能够随手一搜就搜到编译流程主要是:预处理编译汇编链接,但问题是,除了Warning和Error以外一般gcc即便编译成功了都十分沉默,一句话不说的,我怎么直观地知道gcc到底做了什么呢?

  这些细节当然还是有方法能够获得的,比如首先是gcc的编译参数–verbose,它实际上已经足够帮我们看到编译的最关键步骤了,比如我们可以输入:

gcc hello.c --verbose &| vim -
# 或者
gcc hello.c --verbose &> hello.txt

  上面的指令在后台执行,然后让vim读取stdin的输入,之后将gcc的编译日志管道到vim的输入当中,这样就可以在vim当中看到编译日志了,当然这种不方便保存,可以通过后面一条将日志重定向到hello.txt当中,这样就可以保存编译日志的细节了:

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) 
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a-'/usr/lib/gcc/x86_64-linux-gnu/11/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpdir a- -dumpbase hello.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cchifw1I.s
GNU C17 (Ubuntu 11.4.0-1ubuntu1~22.04) version 11.4.0 (x86_64-linux-gnu)compiled by GNU C version 11.4.0, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.1, isl version isl-0.24-GMPGGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/11/include/usr/local/include/usr/include/x86_64-linux-gnu/usr/include
End of search list.
GNU C17 (Ubuntu 11.4.0-1ubuntu1~22.04) version 11.4.0 (x86_64-linux-gnu)compiled by GNU C version 11.4.0, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.1, isl version isl-0.24-GMPGGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 50eaa2331df977b8016186198deb2d18
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a-'as -v --64 -o /tmp/cc3Gumbl.o /tmp/cchifw1I.s
GNU assembler version 2.38 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.38
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a.'/usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper -plugin-opt=-fresolution=/tmp/ccyTXV0s.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. /tmp/cc3Gumbl.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a.'

  编译日志非常长,但是我们仍然能够从中获取到很多关键信息,虽然我没有找到cpp,但是有几条信息比较关键:

/usr/lib/gcc/x86_64-linux-gnu/11/cc1 -quiet ...as -v --64 -o /tmp/cc3Gumbl.o /tmp/cchifw1I.s
GNU assembler version 2.38 (x86_64-linux-gnu) .../usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/ ...

  首先是cc1,它是真正的,狭义上的编译器,它的工作是将C的代码转换为汇编语言代码;后面的两行调用了as,也就是assembler,将上一步生成的汇编代码文件通过as转换为目标代码文件(.o文件);而最后一步调用了collect2完成了最后的链接工作,不过真正的链接器ld的细节可以通过:

gcc hello.c -Wl,--verbose >& hello.txt

  来查看,-Wl会将后面传入的参数传入链接器,所以我们能够从新的日志当中提取出这些信息:

GNU ld (GNU Binutils for Ubuntu) 2.38Supported emulations:elf_x86_64elf32_x86_64.../usr/bin/ld: mode elf_x86_64
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/ ...

  所以编译的最后还会调用ld完成目标代码的链接工作,这一步之后才会生成可执行文件,所以我们大概就清楚GCC在整个流程当中到底做了些什么了,接下来我们就应该研究一下它们的细节了。

(2).预处理

I.预处理会做什么

  上面的日志当中没有显示出预处理的细节,但是它的确是编译会进行的第一步,这一步会处理掉我们常用的很多东西,这一步需要使用cpp,即c preprocessor,C预编译器,我们常用的以#开头的各种语句都会在这一步被处理掉,例如:

#define A 1
#define B 2int main()
{int a = A, b = B;return 0;
}
# 在bash中执行
cpp a.c -o a.i

  在使用cpp完成预处理之后得到的结果是这样:

# 0 "a.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "a.c"int main()
{int a = 1, b = 2;return 0;
}

  我们刚刚加入的两条宏定义在这里被直接通过复制粘贴的方式进行了替换,这也就是预处理器做的最重要的工作:它会将我们不想做的重复操作在编译前全部完成替换,比如我们:

#include <stdio.h>

  它会在预处理阶段把这一千来行代码全都复制粘贴过来,如果写的是C++,包含了iostream,这里可能会有个几万行代码,真的很夸张。

II.预处理器主要包含什么?

  预处理器主要包含这样几个指令:

#include ...
#define ...
#undef ...
#ifdef ...
#else
#endif
#pragma
#error

  在这里我就不细究它们的细节了,不过我们总是能利用预处理器在C语言里得到一些很有意思的事情,比如实际上cpp知道自己处理的文件叫什么名字:

#include <stdio.h>
int main()
{printf("%s\n", __FILE__);return 0;
}

  很不错,它编译出来之后打印出了自己的名字:
在这里插入图片描述
  还有包括__LINE__之类的宏能做到一些很神奇的事情,比如这个去掉一个printf可能就会报错的代码:

#include <stdio.h>
#include <assert.h>
void func()
{printf("Dare you delete this line?\n");assert(__LINE__ % 2 == 0);
}int main()
{func();return 0;
}

在这里插入图片描述
  现在还正常,所以我决定不删掉这一行,我加一行,会发生什么呢?
在这里插入图片描述
  好吧,加一行也直接被断言中止了,实际上这个程序,哪怕在前面删一个空行它都会报错,所以我们可以用这种方式来捉弄一下我们的同学

III.宏的一些魔法

  其实预处理器对于我们来说最神奇的还是宏,它是最基本的直接替换的预处理器,比如我们可以简单写出:

#define A 1

  那么在未来所有的单独出现的A都会在编译期被直接替换成1,所以我们发展出了内联函数的前辈—宏函数,比如大家经常写的:

#define MAX(a, b) ((a) > (b)) ? (a) : (b)
#define ABS(a) ((a) > 0) ? (a) : -(b)

  这都是比较基本的宏,那么在此基础上还有宏操作符#和##能发挥一些神奇的作用,比如:

#define STR(a) #a
#define CONCAT(a, b) a##bint main()
{char* s0 = STR(nihao);char* s1 = CONCAT(nihao, shijie);return 0;
}

  预处理之后的结果如下:

# 0 "c.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "c.c"int main()
{char* s0 = "nihao";char* s1 = nihaoshijie;return 0;
}

  单独一个#会将传入的东西全部转换为字符串字面量,而##则会将前后的两个参数直接拼接到一起,所以我们总是能玩出一些很有意思的东西,比如:

#include <stdio.h>
#define CALL(x) func##x()
void func1() { printf("I'm func1\n"); }
void func2() { printf("I'm func2\n"); }
void func3() { printf("I'm func3\n"); }
int main()
{CALL(1);CALL(2);CALL(3);return 0;
}

  我们用一条简单的宏替换指令完成了三个函数的统一调用,这可要比函数指针数组来的方便多了:
在这里插入图片描述
  但是C的预处理器有一个巨大的问题:宏替换只能发生一次,因此如果我们希望通过一些递归调用的方式完成编译期的运算就做不到了,而正是因为这一点,C语言的宏并不是图灵完备的,而C++的模板最终实现了递归展开,从而达成了图灵完备。

  不过还是有一些魔法能够让C语言做有限次数的递归展开,比如之前我的室友在写《编译原理》作业的时候遇到这样一个问题:

怎么样把一个枚举类型每一个变量的名字打印出来呢?

  好问题,一个比较简单的解决方法是:

#include <stdio.h>
enum {AAA, BBB, CCC, DDD
};const char* enames[] = {"AAA", "BBB", "CCC", "DDD"
};int main()
{printf("%s\n", enames[AAA]);return 0;
}

  这个实现,真的是相当的不优雅呢,每一次如果要添加一个枚举值就要两边同时增加,有没有什么办法能解决这个问题呢?
在这里插入图片描述
  我实在是不想再运行期解决这个问题,所以查阅资料之后发现这样一段非常神奇的代码:

#include <iostream>
#define PARENS ()// Rescan macro tokens 256 times
#define EXPAND(arg) EXPAND1(EXPAND1(EXPAND1(EXPAND1(arg))))
#define EXPAND1(arg) EXPAND2(EXPAND2(EXPAND2(EXPAND2(arg))))
#define EXPAND2(arg) EXPAND3(EXPAND3(EXPAND3(EXPAND3(arg))))
#define EXPAND3(arg) EXPAND4(EXPAND4(EXPAND4(EXPAND4(arg))))
#define EXPAND4(arg) arg#define FOR_EACH(macro, ...)                                    \__VA_OPT__(EXPAND(FOR_EACH_HELPER(macro, __VA_ARGS__)))
#define FOR_EACH_HELPER(macro, a1, ...)                         \macro(a1)                                                     \__VA_OPT__(FOR_EACH_AGAIN PARENS (macro, __VA_ARGS__))
#define FOR_EACH_AGAIN() FOR_EACH_HELPER#define ENUM_CASE(name) case name: return #name;#define MAKE_ENUM(type, ...)                    \
enum type {                                     \__VA_ARGS__                                   \
};                                              \
constexpr const char *                          \
to_cstring(type _e)                             \
{                                               \using enum type;                              \switch (_e) {                                 \FOR_EACH(ENUM_CASE, __VA_ARGS__)              \default:                                      \return "unknown";                           \}                                             \
}MAKE_ENUM(NAME, A, B, C, D);
using namespace std;int main()
{cout << to_cstring(A) << to_cstring(B) << to_cstring(C) << endl;return 0;
}

  它能够让C++的预处理器支持256次递归展开的过程,并且还能优雅地只修改一个MAKE_ENUM里面的参数来实现字符串和枚举值的同步增加:

...
# 2 "enums.cpp" 2
# 35 "enums.cpp"# 35 "enums.cpp"
enum NAME { A, B, C, D }; 
constexpr const char * to_cstring(NAME _e) 
{ using enum NAME; switch (_e) { case A: return "A"; case B: return "B"; case C: return "C"; case D: return "D"; default: return "unknown"; } 
};
using namespace std;int main()
{cout << to_cstring(A) << to_cstring(B) << to_cstring(C) << endl;return 0;
}

  前面的代码太多就省略了,预处理出来的结果不太好看我也整理了一下格式,于是就有了这段很优雅的代码,它会自动展开,生成一个枚举,之后再生成获取对应名字的一个函数,很神奇,但我实在是没有读懂它的原理

(3).编译

I.基本流程

  如果是广义上说的编译,那其实包括了四个流程,但是狭义上说,在C语言当中,编译就是编译器cc1将预处理后的代码完成语法分析、词法分析等一大堆过程,最终生成一个汇编代码文件的过程,所以我们可以直接自己尝试使用cc1完成这个编译操作:

#include <stdio.h>
int main()
{printf("Hello, world!\n");return 0;
}

  首先用cpp完成预处理流程生成.i文件,然后再用cc1完成汇编工作生成.s文件
在这里插入图片描述
  它会输出一些编译的分析日志,然后对应生成的汇编代码如下:

    .file    "hello.i".text.section    .rodata
.LC0:.string    "Hello, world!".text.globl    main.type    main, @function
main:
.LFB0:.cfi_startprocpushq    %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6leaq    .LC0(%rip), %raxmovq    %rax, %rdicall    puts@PLTmovl    $0, %eaxpopq    %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size    main, .-main.ident    "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0".section    .note.GNU-stack,"",@progbits
II.编译优化

  主要内容还是在main标签下的内容,实际上也是简单的函数栈帧建立,然后中间调用puts函数完成打印工作,这就是编译流程做的工作了,实际上这一步我们可以尝试写一点有意思的代码,比如:

#include <stdio.h>
int main()
{int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}printf("sum = %d\n", sum);return 0;
}

  如果我们附加-O0参数进行编译,那得到的汇编代码是这样的(截取了核心部分):

...
main:
.LFB0:.cfi_startprocpushq    %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6subq    $16, %rspmovl    $0, -4(%rbp)movl    $1, -8(%rbp)jmp    .L2
.L3:movl    -8(%rbp), %eaxaddl    %eax, -4(%rbp)addl    $1, -8(%rbp)
.L2:cmpl    $100, -8(%rbp)jle    .L3movl    -4(%rbp), %eaxmovl    %eax, %esi
...

  一眼看上去不是很显然,但是可以明显看到jle指令的使用,所以这串代码完成了一个循环加法的流程,这和我们的代码实现是基本一致的,但是假设这个时候我们在cc1编译的时候打开O1优化,那么代码会变成:

...
main:
.LFB0:.cfi_startprocsubq    $8, %rsp.cfi_def_cfa_offset 16movl    $100, %eax
.L2:subl    $1, %eaxjne    .L2movl    $5050, %esileaq    .LC0(%rip), %rdimovl    $0, %eaxcall    printf@PLTmovl    $0, %eaxaddq    $8, %rsp.cfi_def_cfa_offset 8ret.cfi_endproc
...

  main的代码变短了,而且你可能发现了一个不和谐的东西:

    movl    $5050, %esi

  很离谱,编译器识别出了你尝试从1加到100,然后直接把这个值算出来了,作为一个立即数直接存入参数寄存器,哇哦,这样一来,运行时期的效率就有明显的提升了呢。

III.一点例子

  这里就要提到我之前经历过的一个很有意思的例子了,当时我在尝试演示一个Race Condition问题的代码的时候出了一些问题,代码是这样:

#include <stdio.h>
#include <pthread.h>
#define NUMS 10
int sum = 0;void* T()
{for (int i = 0; i < 1000000; i++) {sum++;}pthread_exit(NULL);
}int main()
{pthread_t thr[NUMS];for (int i = 0; i < NUMS; i++) {pthread_create(&thr[i], NULL, T, NULL);}for (int i = 0; i < NUMS; i++) {pthread_join(thr[i], NULL);}printf("sum = %d\n", sum);return 0;
}

  在我以习惯的编译命令编译运行之后,得到的结果是这样:
在这里插入图片描述
这真的很奇怪啊! 明明结果应该是一个不确定的数字的,为什么都是整的百万呢?在我百思不得其解的时候,我的天才同学想到一个可能:是不是编译器优化了? 我才终于想起来我用的编译命令是:

gcc a.c -o a --save-temps -O2 -pthread

  问题就出在这个-O2身上,我们可以看一看它的汇编代码:

T:
.LFB24:.cfi_startprocendbr64pushq	%rax.cfi_def_cfa_offset 16popq	%rax.cfi_def_cfa_offset 8xorl	%edi, %edisubq	$8, %rsp.cfi_def_cfa_offset 16addl	$1000000, sum(%rip)call	pthread_exit@PLT.cfi_endproc

  我这里把T函数的指令取出来了,这下我算是知道发生什么事情了:这个循环加法被优化成直接给sum加100万了,于是虽然会出现race condition让结果达不到1000万,但是结果也一定是整百万的,所以我赶紧改成了-O0,于是:
在这里插入图片描述
  这就对了,所以这也是比较直观的,可能会遇到编译优化带来程序行为异常的地方。

(4).汇编

  汇编这一步的操作实际上更像是我们尝试用写过的一门语言去执行写另一门语言要做的事情,简单说就是我们的C语言实际上在编译步骤就已经不复存在了,进入汇编阶段的时候,我们做的就是把汇编代码转换成机器代码了,如果我们不写C而直接写汇编代码,实际上也可以直接通过汇编器完成后面的工作,也就是说,从这一步开始,我们处理的对象就已经不再是C语言了,而是汇编语言代码。

  所以我们还是用这段汇编代码来完成汇编工作,这是之前打印hello,world经过编译过程生成的汇编代码文件:

    .file    "hello.i".text.section    .rodata
.LC0:.string    "Hello, world!".text.globl    main.type    main, @function
main:
.LFB0:.cfi_startprocpushq    %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6leaq    .LC0(%rip), %raxmovq    %rax, %rdicall    puts@PLTmovl    $0, %eaxpopq    %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size    main, .-main.ident    "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0".section    .note.GNU-stack,"",@progbits

  然后用as进行汇编操作,可以得到对应的目标代码文件,到这一步实际上就已经是机器语言代码了,我们可以用objdump来反汇编得到对应的汇编代码:
在这里插入图片描述
  其实我们会发现,反汇编重新得到的汇编代码要比前面我们送进汇编器的代码精简很多,它能够更加精简地向我们展示编译出来的文件当中真正启动关键作用的部分是什么。
  我们还可以用file命令查看一下hello.o这个文件的详情:
在这里插入图片描述

  所以hello.o是一个x86-64架构的可重定位的64位ELF文件,我们用hexdump -C来查看一下hello.o:
在这里插入图片描述
  真的是朴实无华的ELF头,它是真的在最初的几个字节保存了ELF几个字母对应的二进制编码,查阅资料后得知:可重定位文件包含了在编译过程中生成的机器代码、符号表、重定位信息等数据。与可执行文件相比,可重定位目标文件并不包含绝对地址,而是使用相对地址和符号引用来表示各个代码段之间的关系。所以这个阶段的文件只是可重定向文件,在没有完成链接步骤之前,是不可以执行的。

(5).链接

  这就是,最后一步了…我们终于走了一串C语言代码变成可执行文件前的最后一步了,链接器会帮助我们把某个程序正常运行需要的所有库(静态库、动态库)、当前项目的所有可执行文件等等统统链接起来,形成一个真正可以直接用来执行的文件,在这里我们还是沿用前一步中已经获得的hello.o文件,这么做:

gcc hello.o -o hello

  这里用回gcc而不是collect2或者ld的原因是:我并不知道我可能要链接的库在哪,所以专业的事情还是交给gcc做吧,所以得到的可执行文件我们可以再用objdump反汇编一下:
在这里插入图片描述
  然后代码长了很多很多,跟刚才的objdump -d hello.o的结果相去甚远,实际上这也就是链接工作的复杂所在,为了让一个函数能够在当前系统环境下运行,我们可能需要调用很多的库,链接器做的也就是这件事:让你需要用的函数能够被程序找到,如果是静态链接则直接放入可执行文件,如果是动态链接则要保证你能找得到在哪,不过关于动态链接的细节我暂时还不明确,未来我应该还会继续探究。

  再用file和hexdump看看hello这个文件:
在这里插入图片描述
  这个文件是64位可执行的ELF文件,采取动态链接方式,我们可以生成一个静态链接和一个动态链接的版本对比一下它们的大小:
在这里插入图片描述
  可以发现,动态链接版本的hello占用15888字节(大约15.5KB),而静态链接的版本则占用900344字节(大约879.2KB),其实最主要的区别就在于静态链接会将需要用到的库放入可执行文件当中,因此生成的可执行文件可能会非常大

  哦对了,还有一件事:
在这里插入图片描述

  一个鲜活的Hello, world! 在此时终于呈现在你的面前了,你是否会发出一些感慨呢?我们早期学习编程的过程中在IDE中随手一点的编译运行,背后竟然还有这么多这么复杂的过程。

(6).说到这里,为什么我们要用gcc呢?

  我想到这儿其实问题的答案已经很明显了:cpp、cc1、as、ld这几条命令的确是可以让我们手动执行,但是这样的编译过程很明显更方便一点:

gcc hello.c -o hello

  作为一个完备的编译器,它帮我们做完了我们可能需要完成的全部操作,直接就可以得到一个可执行文件,我们也可以给它附加非常非常多参数来适应我们的要求,比如:

gcc hello.c -E -o hello.i # 得到预处理结果
gcc hello.c -S -o hello.s # 得到编译结果
gcc hello.s -c -o hello.o # 得到汇编结果
gcc hello.o -o    hello   # 得到可执行文件
# 以及更多
gcc hello.c -o hello --save-temps -Wall -Werror -Wl,--verbose -fsanitize=address

  gcc真的为我们提供了非常多可以用的编译指令,让我们能够以更加轻松的方式完成对于程序的编译流程,fsanitize选项甚至可以帮助我们在编译期检查各种各样可能出现的问题,这极大增强了开发人员在编译过程中发现代码漏洞的能力。

3.还有别的选择吗?

  当然,编译器不能一家独大,实际上基于LLVM的Clang/Clang++以及微软主推的MSVC都是目前市场上非常流行的编译器组件,C/C++的标准化委员会实际上不存在一个官方编译器,所有的编译器都是在标准推出之后由编译器厂商自主实现的,所以委员会定的标准,编译器厂商不一定听;委员会没有定的标准,编译器厂商也可能自己会加

  一个比较常见的例子就是__attribute__(),这是独属于GCC的编译指令,可以通过这一系列指令完成对于编译器的控制,比如要求禁止内联等等,这是一个对于编译器的强制要求,有的时候:

inline void func()
{printf("I'm a simple function!\n");
}

  编译器可能不会听你的,把这个函数作为一个内联函数像是宏函数一样粘贴到你调用它的位置,但你要是用__attribute__((noinline)),编译器是肯定不会把你的函数作为内联函数进行优化的。

4.杂谈

(1).自己编译一个GCC

  这事儿我还真做过,还做过不止一次,实际上GCC的代码量很大,编译它的工作大约需要消耗半个小时左右,其实已经不算长了,因为让人难以想象的是:Chromium内核的编译可能需要6~8个小时,它甚至比编译Linux内核要用的时间要长得多。

  它现在在我的WSL上安然地跑着,帮助我完成很多工作:
在这里插入图片描述
  其实过程相当简单,甚至我说和Lab0编译risc-v工具链的流程都没有什么区别,你只要从gcc.gnu.org的git源下载到最新的源码,然后按照指南进行make即可。

(2).构建系统和Makefile

  最后还想提一提构建系统和Makefile,编译一个大型项目对于C/C++理论上讲是个很头痛的过程,假设一个项目的依赖关系特别复杂,假设修改过一个文件,要想完成整个项目的重新编译就要耗费相当大的经历,因此构建系统基本上就是来完成这个工作的:我们作为程序员把程序的依赖关系理清楚,后面再想编译,就全部交给构建系统就好了

  早期出现的是Makefile,要说它是构建系统,我觉得它更像一个自动化工具,它实际上只会完成你让它做的事情,但是这并没有阻碍它成为一个良好的构建工具,例如:

a : a.cgcc a.c -o a --save-temps -O2 -ggdbclean:rm a.o a.s a.i a

  就这样我就可以通过make a && ./a的方式一键编译运行了,想要清楚掉编译产生的文件也只需要make clean即可。不过大家还是嫌Makefile太麻烦,于是出现了cmake、qmake、xmake、ninja等等一系列的各种构建系统,它们能够帮助我们更方便地完成项目依赖关系的构建。

总结

  这一次的作业实质上探讨了一下GCC编译器的一些细节以及编译一个C程序会经历的预处理、编译、汇编和链接的全部流程,这也说明,编译器或许的确也是人类程序员智慧的结晶,毕竟归根结底,即便是操作系统,也不过是在机器上运行的一条条指令构成的程序罢了,当我们用C语言实现一个操作系统的时候,我们还是需要使用编译器来帮助我们完成全部的编译工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/822018.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解剖this指针

目录 this指针的理解 1. this指针的用处 2.this指针的使用 3.this指针的使用 this指针来源 this指针的理解 通常在class定义时要用到类型变量自身时&#xff0c;因为这时候还不知道变量名&#xff08;为了通用也不可能固定实际的变量名&#xff09;&#xff0c;就用this这样…

windows网络驱动开发

基石&#xff1a;WFP 1、简介 Windows过滤平台&#xff08;Windows Filtering Platform, WFP&#xff09;&#xff0c;是从Vista系统后新增的一套系统API和服务。开发者可以在WFP框架已划分的不同分层中进行过滤、重定向、修改网络数据包&#xff0c;以实现防火墙、入侵检测系…

电机控制器电路板布局布线参考指导(五)

电机控制器电路板布局布线参考指导&#xff08;五&#xff09;大容量电容和旁路电容的放置 1.大容量电容的放置2.电荷泵电容器3.旁路电容/去耦电容的放置3.1 靠近电源3.2 靠近功率器件3.3 靠近开关电流源3.4 靠近电流感测放大器3.5 靠近稳压器 tips&#xff1a;资料主要来自网络…

Modality-Aware Contrastive Instance Learning with Self-Distillation ... 论文阅读

Modality-Aware Contrastive Instance Learning with Self-Distillation for Weakly-Supervised Audio-Visual Violence Detection 论文阅读 ABSTRACT1 INTRODUCTION2 RELATEDWORKS2.1 Weakly-Supervised Violence Detection2.2 Contrastive Learning2.3 Cross-Modality Knowle…

读天才与算法:人脑与AI的数学思维笔记02_激发创造力

1. 心理创造力 1.1. 自我创造力的实现结果对个体来说可能是全新的&#xff0c;但纵观历史其实已算是“明日黄花”&#xff0c;这就是心理创造力的概念 2. 激发创造力 2.1. 理智是创造力最大的敌人 2.1.1. 巴勃罗毕加索&#xff08;P…

Jmeter 场景测试:登录--上传--下载--登出

为了练习Jmeter的使用&#xff0c;今天我要测试的场景是“登录--上传--下载--登出”这样一个过程. 测试的目标是我曾经练手写的一个文件分享系统&#xff0c;它要求用户只有登录后才可以下载想要的文件。 Jmeter总体结构&#xff1a; 第一步&#xff1a;添加HTTP Cookie管理器…

47.HarmonyOS鸿蒙系统 App(ArkUI)创建轮播效果

创建轮播效果&#xff0c;共3页切换 Entry Component struct Index {State message: string Hello Worldprivate swiperController: SwiperController new SwiperController()build() {Swiper(this.swiperController) {Text("第一页").width(90%).height(100%).bac…

电动汽车退役锂电池SOC主动均衡控制MATLAB仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 仿真简介 模型选用双向反激变换器作为主动均衡拓扑电路&#xff0c;均衡策略采用基于SOC的主动均衡策略&#xff0c;旨在解决电动汽车退役锂电池的不一致性问题。模型选用双向反激变换器作为主动均衡拓扑电路…

pytorch Neural Networks学习笔记

&#xff08;1&#xff09;输入图像&#xff0c;13232&#xff0c;通道数1&#xff0c;高32&#xff0c;宽32 &#xff08;2&#xff09;卷积层1&#xff0c;滤波器的shape为6155&#xff0c;滤波器个数6&#xff0c;通道数1&#xff0c;高5&#xff0c;宽5。卷积层1的输出为62…

【介绍下负载均衡原理及算法】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

使用docker配置DSP-SLAM

一.Docker环境配置 1.简单介绍 –docker容器技术–。 简单理解&#xff1a;Anaconda用于隔离不同的python环境&#xff1b;docker可以理解成在你的机器里面安装了一个独立的系统&#xff0c;因此它可以隔离不同的CUDA环境&#xff0c;还有着独立的文件系统&#xff0c;防止别…

高级IO和5种IO模型

目录 1. 高级IO1.1 IO的基本概念1.2 OS如何得知外设当中有数据可读取1.3 OS如何处理从网卡中读取到的数据包1.4 IO的步骤 2. 五种IO模型2.1 利用钓鱼来理解2.2 阻塞IO2.3 非阻塞IO2.4 信号驱动IO2.5 IO多路转接2.6 异步IO 3. 高级IO的概念3.1 同步通信 VS 异步通信3.2 阻塞 VS …

k-means聚类算法的MATLAB实现及可视化

K-means算法是一种无监督学习算法&#xff0c;主要用于数据聚类。其工作原理基于迭代优化&#xff0c;将数据点划分为K个集群&#xff0c;使得每个数据点都属于最近的集群&#xff0c;并且每个集群的中心&#xff08;质心&#xff09;是所有属于该集群的数据点的平均值。以下是…

STM32有什么高速接口吗?

STM32系列微控制器在高速接口方面也提供了一些强大的功能&#xff0c;虽然没有像Zynq那样的可编程逻辑部分&#xff0c;但有一些特性值得注意。我这里有一套嵌入式入门教程&#xff0c;不仅包含了详细的视频 讲解&#xff0c;项目实战。如果你渴望学习嵌入式&#xff0c;不妨点…

【数据结构与算法】用两个栈实现一个队列

题目 用两个栈&#xff0c;实现一个队列功能 add delete length 队列 用数组可以实现队列&#xff0c;数组和队列的区别是&#xff1a;队列是逻辑结构是一个抽象模型&#xff0c;简单地可以用数组、链表实现&#xff0c;所以数组和链表是一个物理结构&#xff0c;队列是一个逻…

Go 单元测试基本介绍

文章目录 引入一、单元测试基本介绍1.1 什么是单元测试&#xff1f;1.2 如何写好单元测试1.3 单元测试的优点1.4 单元测试的设计原则 二、Go语言测试2.1 Go单元测试概要2.2 Go单元测试基本规范2.3 一个简单例子2.3.1 使用Goland 生成测试文件2.3.2 运行单元测试2.3.3 完善测试用…

easyexcel升级3.3.4失败的经历

原本想通过easyexcel从2.2.6升级到3.3.3解决一部分问题&#xff0c;结果之前的可以用的代码&#xff0c;却无端的出现bug 1 Sheet index (1) is out of range (0…0) 什么都没有改&#xff0c;就出了问题&#xff0c;那么问题肯定出现在easyexcel版本自身.使用模板填充的方式进…

conda新建环境报错An HTTP error occurred when trying to retrieve this URL.

conda新建环境报错如下 cat .condarc #将 .condarc文件中的内容删除&#xff0c;改成下面的内容 vi .condarc channels:- defaults show_channel_urls: true default_channels:- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main- https://mirrors.tuna.tsinghua.…

权限管理Ranger详解

文章目录 一、Ranger概述与安装1、Ranger概述1.1 Ranger介绍1.2 Ranger的目标1.3 Ranger支持的框架1.4 Ranger的架构1.5 Ranger的工作原理 2、Ranger安装2.1 创建系统用户和Kerberos主体2.2 数据库环境准备2.3 安装RangerAdmin2.4 启动RangerAdmin 二、Ranger简单使用1、安装 R…

【Java NIO】那NIO为什么速度快?

Java IO在工作中其实不常用到&#xff0c;更别提NIO了。但NIO却是高效操作I/O流的必备技能&#xff0c;如顶级开源项目Kafka、Netty、RocketMQ等都采用了NIO技术&#xff0c;NIO也是大多数面试官必考的体系知识。虽然骨头有点难啃&#xff0c;但还是要慢慢消耗知识、学以致用哈…