上一篇文章简单提到了编译过程。对于笔者而言,C++编译运行似乎只是在IDE界面按下F5按钮,然后把一切事情交给编译器,然后等待正确运行,在终端开始交互测试,或者看着出现的warning和error开始漫长的debug。。。从未想过背后编译器都做了什么事。实际上想成为一个真正的底层开发者,应该学会和编译器打交道。

很遗憾在本科专业学习中并没有《编译原理》这门课程,也没有特地研究过这门学问,只是在一次又一次的debug中获得只言片语的了解。想在短时间内掌握这门技术是很困难的,也难以在一篇文章里说清楚来龙去脉。所以笔者从一个简单的C++ 程序展开,一步一步探究程序员使用的高级语言是如何被一层一层“翻译”直到变成计算机可以读懂并执行的字节的。

笔者写了一个最简单的C++程序,内容是大家在学习无论何种计算机语言的时候通常会写的第一个demo:hello world。这里没有使用using namespace,语句能达成最基本功能即可。囿于这方面的知识有限,分析过程中还请读者指出错误。

int 

C/C++编译是集成的,笔者常用gcc/g++ [source file] -o [executable file]。其中隐藏了很多步骤,拆分开来是以下四个步骤:

  1. 预处理(preprocessing):展开头文件、宏替换、去掉注释、条件编译,产生.i后缀文件,gcc -E helloworld.c -o helloworld.i
  2. 编译(compression):检查语法、生成汇编,产生.s后缀文件,gcc -S helloworld.i -o hello.s
  3. 汇编(assembly):汇编代码转换成机器码,产生.o后缀文件,gcc -S helloworld.i -o helloworld.s
  4. 连接(linking):连接到一起生成可执行文件,产生.out后缀文件,gcc -c helloworld.s -o helloworld.o

先来看第一个步骤预处理的结果,helloworld.i。由于遵循C语言文法,它仍然是C文件。文件有733行,故节选部分笔者认为较有价值的片段进行分析。

首先笔者对define进行了全局搜索,没有结果,可以印证预处理过程中对所有define都完成了变量替换。helloworld.i的最前面是一系列和stdio.h有引用关系的头文件路径。

# 1 "helloworld.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 31 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<命令行>" 2
# 1 "helloworld.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 450 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 460 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 461 "/usr/include/sys/cdefs.h" 2 3 4
# 1 "/usr/include/bits/long-double.h" 1 3 4
# 462 "/usr/include/sys/cdefs.h" 2 3 4
# 451 "/usr/include/features.h" 2 3 4
# 474 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
# 475 "/usr/include/features.h" 2 3 4
# 34 "/usr/include/bits/libc-header-start.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4

接下来是一系列常用数据类型的别名定义。

typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;typedef signed long int __int64_t;
typedef unsigned long int __uint64_t

IO_FILE的文件定义。

typedef struct _IO_FILE FILE;
# 43 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/include/bits/types/struct_FILE.h" 1 3 4
# 35 "/usr/include/bits/types/struct_FILE.h" 3 4
struct _IO_FILE;
struct _IO_marker;
struct _IO_codecvt;
struct _IO_wide_data;
typedef void _IO_lock_t;
struct _IO_FILE
{int _flags;char *_IO_read_ptr;char *_IO_read_end;char *_IO_read_base;char *_IO_write_base;char *_IO_write_ptr;char *_IO_write_end;char *_IO_E_base;char *_IO_buf_end;char *_IO_save_base;char *_IO_backup_base;char *_IO_save_end;struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno;int _flags2;__off_t _old_offset;unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];_IO_lock_t *_lock;__off64_t _offset;struct _IO_codecvt *_codecvt;struct _IO_wide_data *_wide_data;struct _IO_FILE *_freeres_list;void *_freeres_buf;size_t __pad5;int _mode;char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

三个常用标准输入输出流文件

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

之后是大量的文件处理(fseek、fread、fwrite等)、输入输出函数(printf、sprintf、fprintf等)声明,此处截取部分。补充以下此处使用extern关键字,使该函数在实现部分使用原来文件里面的定义。

extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);
extern int getchar (void);
extern int getc_unlocked (FILE *__stream);
extern int getchar_unlocked (void);
# 510 "/usr/include/stdio.h" 3 4
extern int fgetc_unlocked (FILE *__stream);
# 521 "/usr/include/stdio.h" 3 4
extern int fputc (int __c, FILE *__stream);
extern int putc (int __c, FILE *__stream);
extern int putchar (int __c);
# 537 "/usr/include/stdio.h" 3 4
extern int fputc_unlocked (int __c, FILE *__stream);
extern int putc_unlocked (int __c, FILE *__stream);
extern int putchar_unlocked (int __c);
extern int getw (FILE *__stream);
extern int putw (int __w, FILE *__stream);

最后才是笔者写的程序本体。

可见,预处理阶段,编译器将代码中的stdio.h编译进来,把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。

接着对该文件进行编译处理,这一步编译器检查是否有语法错误,准确无误则开始。结果如下(不太懂Assembly,不作注释,maybe I will add them someday... )。

  .file   "helloworld.c".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), %rdimovl    $0, %eaxcall    printf@PLTmovl $0, %eaxpopq    %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size main, .-main.ident  "GCC: (GNU) 9.2.0".section    .note.GNU-stack,"",@progbits

汇编阶段,汇编语言被翻译为机器语言,二进制文件。当笔者打算查看此helloworld.o文件的时候,无论是terminal还是VSCode都对笔者发出了“是否继续查看”的提示。果然人类无法阅读:

最后一步是连接,完成这一步即可得到一个可以运行的可执行文件了。但是稍等,有一个问题,“连接”的是什么东西?所需的外部文件在预处理阶段就已经包括进来,为什么还需要引入外部文件?所以提出一个关键概念——链接库。库有两种,一种是静态链接库,一种是动态链接库,不管是哪一种库,要使用它们,都要在程序中包含相应的include头文件。linux中,静态库文件后缀.a,动态库文件后缀.so。

什么是静态链接呢?即在链接阶段,将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件可能会比较大。这种链接方式的好处是:方便程序移植,因为可执行程序与库函数再无关系,放在如何环境当中都可以执行。缺点是:文件太大。想尝试的话可以在编译参数中加上-static。

那么什么是动态连接呢?我们知道静态链接的话,文件会很大,往往实现很小的一个功能就需要占用很大的空间,而且每次库文件升级的话,都要重新编译源文件,很不方便。而且静态文件在内存中多份拷贝,占用很大空间。gcc默认先采用动态库。但是动态库在内存中存在一份即可。动态链接有一个缺点就是可移植性太差,如果两台电脑运行环境不同,动态库存放的位置不一样,很可能导致程序运行失败。所以应该结合实际环境考虑采用何种库。

此处引用一下yanlei的回答(thanks)

yanlei:编译器编译原理:预处理,编译,汇编,链接各步骤详解​zhuanlan.zhihu.com

静态库链接时搜索路径顺序:

  • 1. ld会去找GCC命令中的参数-L
  • 2. 再找gcc的环境变量LIBRARY_PATH
  • 3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile gcc时写在程序内的

动态链接时、执行时搜索路径顺序:

  • 1. 编译目标代码时指定的动态库搜索路径
  • 2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  • 3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  • 4. 默认的动态库搜索路径/lib
  • 5. 默认的动态库搜索路径/usr/lib

有关环境变量:

  • LIBRARY_PATH环境变量:指定程序静态链接库文件搜索路径
  • LD_LIBRARY_PATH环境变量:指定程序动态链接库文件搜索路径

那么笔者再写一个例子,说明这两种库的使用。

写两个文件,add.h和add.c,并且目录结构如下所示:

[jaimeow@jaimeow-pc demo2]$ ls -R
.:
add.c  addlib  add.o  test  test.c
./addlib:
add.h  libadd.a

#ifndef ADD_H_
#define ADD_H_
int add(int a,int b);
#endif

#include ".addlib/add.h"
#include <stdio.h>
int add(int a,int b){return a + b;
}

然后执行gcc -c add.c和ar -crv libadd.a add.o,生成add.o(目标文件)和libadd.a(静态库文件)。把库文件封装在addlib目录下,再写一个测试样例。

#include "./addlib/add.h"
#include <stdio.h>int main(){int a = 1,b = 2;printf("%d",add(a,b));return 0;
}

结果可以正确运行,输出3。

那么来试一下动态库,gcc -fPIC -shared -o libadd.so add.c,gcc -o test test.c -L./addlib -ladd。如果把libadd.so放入刚才的addlib目录中提示找不到共享库,放入可执行文件所在目录即可正确运行,输出3。从这一点来看,的确静态库是运行之前包括的,动态库是运行时加载的。

写完这篇文章,对计算机语言的认识更进一步,程序员在编程的时候首先要做到了解自己所用的工具,才能更好的使用工具,让它变成你的开发之友。这也是区分程序员水平之处。

(如有转载请注明作者与出处,欢迎建议和讨论,thanks)

C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起相关推荐

  1. C++报错无效的预处理命令include_无废话--Mac OS, VS Code 搭建c/c++基本开发环境

    无废话,直接上步骤. 1) 安装 xcode. 打开App Store,搜索xcode,进行下载安装. 2)执行命令: xcode-select --install 安装命令行工具. 3)安装VS C ...

  2. Mybatis中sql语句报错无效参数类型问题

    报错mybatis无效参数类型问题,mybatis中sql语句的参数,如果这个参数可以为空,那么则必须添加jdbcType,否则将报错无效参数类型. Mybatis文档中有如下解释: 像 MyBati ...

  3. java exec执行tar_用java调用rpmbuild 报错,同一条命令直接复制到终端却能运行

    用java调用rpmbuild 报错,同一条命令直接复制到终端却能运行. 命令如下: rpmbuild --define "_topdir /var/lib/jenkins/workspac ...

  4. fastqc检验时不能执行java_解压fastqc软件包后,运行fastqc报错:没有这个命令?...

    最近在做CHIP-seq,从NCBI上获取了原始数据后,想用fastqc检查一下二代测序数据有没有问题 于是我从官网上面下载了fastqc人软件包,并解压到了Biosofts文件夹里面 然后运行 fa ...

  5. Linux CentOS 7安装之后,ip addr命令无法显示ip地址。ifconfig命令报错:未找到命令!

    文章目录 一.Linux CentOS 7安装之后,ip addr命令无法显示ip地址.ifconfig命令报错:未找到命令! 二.解决"ip addr命令无法显示ip地址"方法 ...

  6. manjaro 更新报错-无效或已损坏的软件包 (PGP 签名)

    @[TOC](manjaro 更新报错-无效或已损坏的软件包 (PGP 签名)) 原因分使用了社区源 并开启了验证,关闭验证即可 vim /etc/pacman.conf [archlinuxcn] ...

  7. 代码在eclipse下不报错,在doc命令行下报错--jar file和runable jar file

    今天开发一个小工具,引用了Log4j,来记录日志,在eclipse下运行,代码正常,打包成jar放到doc命令行下运行报错: Exception in thread "main" ...

  8. python打开文件报错无效序列_黑马python入门(4):python基础(序列,异常,操作文件,模块包,日志调试信息)

    序列 str声明:test_str="abcedf" 也可以保留字符串里面的格式来 test_str=""" \r\n测试标题 hello world ...

  9. 【报错】java -jar 命令启动后中文乱码

    文章目录 报错 解决 报错 我们在Windows下运行jar包时,常常会出现乱码,主要分为dos窗口输出的日志中出现乱码和程序返回数据出现乱码. 解决 一.dos窗口输出的日志中出现乱码 执行如下命令 ...

最新文章

  1. pandas使用replace函数将dataframe指定数据列中的特定字符串进行自定义替换(replace substring in dataframe column values)
  2. 关于meta的一些知识
  3. 队列的C语言实现(通过内核链表)
  4. 互联网1分钟 | 0117 IBM入驻上海张江人工智能岛;IoT业务将成为小米新支撑点
  5. mysql 逻辑处理_mysql 逻辑查询处理流程
  6. Java web后端6 java Bean EL表达式
  7. python需要配置环境变量吗_python安装和配置环境变量
  8. 【网络基础】《TCP/IP详解》学习笔记5
  9. linux中怎么创建管道文件,Linux  管道文件
  10. [Unity][摄像机]动态代码设置Camera的CullingMask遮罩
  11. 硬盘格式化后数据能不能恢复,硬盘格式化数据怎么恢复
  12. Android触控签名软件,Android Sign Kit(app一键签名)
  13. sql server 2000 打了sp4补丁包仍不能监听1433端口问题的解决
  14. 《21世纪资本论》阅读摘要
  15. 01-初探MQ-MQ的三大使用场景:应用解耦、异步提速、削峰填谷
  16. 深度强化学习训练调参方法
  17. 这是我见过的最垃圾的代码,没有之一
  18. linux开热点软件,Debian开WI-FI热点
  19. OpenCV + CPP 系列(九)颜色空间
  20. python--转换wrf输出的风场数据为网页可视化的json格式

热门文章

  1. 优酷路由器-openwrt学习二
  2. Visdrone2019数据集.txt标签文件转换为voc格式.XML标签文件
  3. 设计模式:工厂设计模式
  4. upload-labs18关
  5. 华为交换机M-LAG配置
  6. 年关将至业内警示P2P跑路风险
  7. 而多乐在线书签导入html文件,,简单介绍HTML5中的文件导入
  8. 【各种问题系列】Oracle11g oracle net configuration assistant 报错:不能创建监听程序
  9. 基于Springboot 的实验室检测信息管理系统
  10. JMP学习知识库,知识酷!