笔记:Linux环境C语言复习(1)//C语言编译过程

浪尽此生 提交于 2020-02-29 19:34:10

目前对C语言感性的理解:C语言,就是一门语言,和英语、日语一样。本科时候学过谭浩强的红皮书,那是一本语法书,和中学的英文语法书没有太大的区别,规定一系列的约定俗成的规范(复数名词后要加s;定义整型变量前面要用int修饰),就是强行记忆,没有什么捷径。语法很重要,学会语法是进入这个语系世界的钥匙。但是,学会语法和学会语言是两件事情。我和刘慈欣都熟练掌握中文语法,但是他能写《三体》获得雨果奖,而我写个开题报告都要被导师骂一顿(苦笑)。语言能力的高低,最终还是体现在思维的深度和广度,语法不过是一种人机的通讯协议。自知能力有限,不敢谈C语言,只是总结一些C语言语法和原理以供日后复习。

C语言编译过程
在Linux环境下,我们编译一个.c文件一般直接使用gcc编译器执行“gcc 文件名.c” 就可以获取二进制可执行文件a.out,其实编译器帮我们完成了很多任务。以编译unicorn.c文件为例,gcc编译器实际编译生成二进制可执行文件的过程如下:
1)gcc -E unicorn.c -o unicorn.i
//编译器执行与处理指令
2)gcc -S unicorn.i -o unicorn.s
//编译器对预处理文件进行编译,生成汇编文件
3)gcc -c unicorn.s -o unicorn.o
//编译器对汇编文件进行汇编,生成二进制文件
//通常第二、三步可统一为一步称为编译
4)gcc unicorn.o -o unicorn
//编译器将二进制文件与其依赖进行链接,并与运行时文件绑定,生成可执行的二进制文件

举例:以一个简单计算程序为例,看看每一步编译器的具体工作
代码:

myMath.h:

  1 # ifndef __MYMATH_H__
  2 # define __MYMATH_H__
  3 int add(int i, int j);
  4 double div(int i, int j);
  5 int sub(int i, int j);
  6 int mul(int i,int j)7 # endif            

myMath.c:

1 # include "myMath.h"                        
2
3 int add(int i, int j){
4     return i + j;
5 }
6 
7 int mul(int i, int j){
8     return i * j;
9 }
10 
11 int sub(int i, int j){
12     return i - j;
13 }
14 
15 double div(int i, int j){
16     return i / j;
17 }

myMain.c:

1 #include <stdio.h>
2 #include "myMath.h
3
4 int main()
5 {
6      int i = 10;
7      int j = 3;
8      printf(" i + j = %d\n",add(i, j));
9      printf(" i - j = %d\n",sub(i, j));
10     printf(" i * j = %d\n",mul(i, j));
11     printf(" i / j = %d\n",div(i, j));
12     return 0;
13 }

执行gcc -E myMain. -o myMain.i,并打开myMain.i:

539 
540 int add(int i, int j)541 double div(int i, int j);
542 int sub(int i, int j);
543 int mul(int i,int j);
544 # 3 "myMain.c" 2
545 
546 int main()
547 {
548     int i = 10;
549     int j = 3;
550     printf(" i + j = %d\n",add(i, j));
551     printf(" i - j = %d\n",sub(i, j));
552     printf(" i * j = %d\n",mul(i, j));
553     printf(" i / j = %lf\n",div(i, j));
554     return 0;
555 }  

与myMain.c对比,经过预处理后,原本include包含的头文件名,被实际头文件内容代替

执行gcc -S myMain.i -o myMain.s,并打开myMain.s:

  1     .section    __TEXT,__text,regular,pure_instructions                                                                                                                          
  2     .build_version macos, 10, 14    sdk_version 10, 14
  3     .globl  _main                   ## -- Begin function main
  4     .p2align    4, 0x90
  5 _main:                                  ## @main
  6     .cfi_startproc
  7 ## %bb.0:
  8     pushq   %rbp
  9     .cfi_def_cfa_offset 16
 10     .cfi_offset %rbp, -16
 11     movq    %rsp, %rbp
 12     .cfi_def_cfa_register %rbp
 13     subq    $32, %rsp
 14     movl    $0, -4(%rbp)
 15     movl    $10, -8(%rbp)
 16     movl    $3, -12(%rbp)
 17     movl    -8(%rbp), %edi

预处理文件myMain.i经过编译生成汇编文件

执行gcc -c myMain.s -o myMain.o
再执行ls -l myMain.o:

Desktop Linraffe$ ls -l myMain.o
-rw-r--r--  1 Linraffe  staff  1100  2 29 17:15 myMain.o

发现经过编译起汇编后,生成的二进制文件并不能执行。

执行gcc -c myMain.o myMath.o -o myMain,
在执行myMain:

Desktop Linraffe$ gcc myMath.o myMain.o -o myMain
Desktop Linraffe$ myMain
 i + j = 13
 i - j = 7
 i * j = 30
 i / j = 3

经过链接后,生成的可执行文件成功执行。

对于链接过程的理解,可以执行nm命令:

Desktop Linraffe$ nm myMain.o
                 U _add
                 U _div
0000000000000000 T _main
                 U _mul
                 U _printf
                 U _sub
Desktop Linraffe$ nm myMath.o
0000000000000000 T _add
0000000000000060 T _div
0000000000000020 T _mul
0000000000000040 T _sub
Desktop Linraffe$ nm myMain
0000000100000000 T __mh_execute_header
0000000100000e40 T _add
0000000100000ea0 T _div
0000000100000ec0 T _main
0000000100000e60 T _mul
                 U _printf
0000000100000e80 T _sub
                 U dyld_stub_binder

nm命令+“二进制文件名”:查看该程序实现所依赖的函数
其中,T代表文件中包含函数的实现,U代表文件依赖但是不包含函数的实现。可以看到,myMain.o中并不包含几个函数(add()、su b()…)的实现,而经过链接后,myMain中包含这些函数的实现,从而可以执行。


总结:一个.c文件要经过预处理、编译、汇编、链接,最终才会生成一个可执行的二进制文件。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!