程序的编译(3/13)

chatgpt/2023/10/4 7:51:50

经过预处理后的源文件,褪去一切包装,注释被删除,预处理命令也基本上被处理掉,剩下的就是 C 代码了。接下来的第二步,就进入到编译阶段。编译阶段主要分为两步:第一步,编译器调用一系列解析工具分析这些 C 代码,将 C 源文件编译为汇编文件;第二步,通过汇编器将汇编文件汇编成可重定位的目标文件。

文章目录

    • 从 C 文件到汇编文件
      • 词法分析
      • 语法分析
      • 语义分析
      • 生成中间代码
    • 汇编过程
    • 符号表与重定位表
      • 符号表
      • 重定位表

从 C 文件到汇编文件

一个汇编文件是以段为单位来组织程序的:代码段、数据段、BSS 段等,各个段之间相互独立,并且其和二进制目标文件的组织结构已经很接近了,因为汇编指令是二进制指令的助记符,只不过汇编语言的程序结构需要使用各种伪操作来组织,汇编文件经过汇编器汇编后,处理掉各种伪操作命令,就是二进制目标文件了。

从 C 源文件到汇编文件的转换,起始就是将 C 文件中的程序代码块、函数转换为汇编程序中的代码段,将 C 程序中的全局变量、静态变量、常量转换为汇编程序中的数据段、只读数据段。总体来讲,编译过程可以分为以下 6 步:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 中间代码生成
  5. 汇编代码生成
  6. 目标代码生成

词法分析

主要用来解析 C 程序语句。词法分析一般会通过词法扫描器从左到右,一个字符一个字符地读入源程序,通过有限状态机解析并识别这些字符流,将源程序分解为一系列不能再分解的记号单元 —— token。

token 是字符流解析过程中有意义的最小记号单元,常见的 token 如下:

  • C 语言的各种关键字:int、float、for、while、break 等。
  • 用户定义的各种标识符:函数名、变量名、标号等。
  • 字面量:数字、字符串等。
  • 运算符:C 语言标准定义的 40 多个运算符。
  • 分隔符:程序结束符分号、for循环中的逗号等。

语法分析

语法分析是对前一阶段产生的 token 序列进行解析,看是否能构建成一个语法上正确的语法短语(程序、语句、表达式等)。语法短语用语法树表示,是一种树型结构,不再是线性序列。语法分析工具在对 token 序列分析过程中,如果发现不能构建语法上正确的语句或表达式,就会报语法错误。

语义分析

语法分析仅仅对程序做语法检查,对程序、语句的真正意义不了解,而语义分析主要对语法分析输出的各种表达式、语句进行检查,看看有没有错误。如果你传递给函数的实参与函数声明的形参类型不匹配,或者你使用了一个未声明的变量,或者除数为零了,break 在循环语句或 switch 语句之外出现了,或者在循环语句之外发现了 continue 语句,一般都会报语义上的错误或警告。

生成中间代码

在语法分析阶段输出的表达式或程序语句,还是以语法树的形式存储,我们需要将其转换为中间代码。中间代码是编译过程中的一种临时代码,常见的有三地址码P-代码

中间代码和语法树相比,有很多优点:中间代码是一维线性结构,类型伪代码,编译器很容易将中间代码翻译成目标代码。

jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.c
int main(void)
{int sum = 0;int a = 2;int b = 1;int c = 1;sum = a + b / c;return 0;
}
jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-gcc -fdump-tree-gimple main2.c
jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.c.005t.gimple # 自动生成同名文件
main ()
{int D.4205;{int sum;int a;int b;int c;sum = 0;a = 2;b = 1;c = 1;_1 = b / c;sum = a + _1;D.4205 = 0;return D.4205;}D.4205 = 0;return D.4205;
}

C 程序语句 sum=a+b/c; 编译为三地址码后,就变成了上面所示的类似伪代码的语句。中间码一般和平台是无关的,如果你想将 C 程序编译为 X平台下的可执行文件,那么最后一步就是根据 X86 指令集,将中间代码翻译为 X86 汇编程序;如果你想编译成 ARM 平台上运行的可执行文件,那么就要参考 ARM 指令集,根据 ATPCS 规则分配寄存器,将中间代码翻译成 ARM 汇编程序。

jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-gcc -S main2.c
jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main2.s # 自动生成该后缀文件.arch armv5t.eabi_attribute 20, 1.eabi_attribute 21, 1.eabi_attribute 23, 3.eabi_attribute 24, 1.eabi_attribute 25, 1.eabi_attribute 26, 2.eabi_attribute 30, 6.eabi_attribute 34, 0.eabi_attribute 18, 4.file	"main2.c".text.global	__aeabi_idiv.align	2.global	main.syntax unified.arm.fpu softvfp.type	main, %function
main:@ args = 0, pretend = 0, frame = 16@ frame_needed = 1, uses_anonymous_args = 0push	{fp, lr}add	fp, sp, #4sub	sp, sp, #16mov	r3, #0str	r3, [fp, #-20]mov	r3, #2str	r3, [fp, #-16]mov	r3, #1str	r3, [fp, #-12]mov	r3, #1str	r3, [fp, #-8]ldr	r1, [fp, #-8]ldr	r0, [fp, #-12]bl	__aeabi_idivmov	r3, r0mov	r2, r3ldr	r3, [fp, #-16]add	r3, r3, r2str	r3, [fp, #-20]mov	r3, #0mov	r0, r3sub	sp, fp, #4@ sp neededpop	{fp, pc}.size	main, .-main.ident	"GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0".section	.note.GNU-stack,"",%progbits

汇编过程

汇编过程是使用汇编器将前一阶段生成的汇编文件翻译成目标文件。汇编器的主要工作就是参考 ISA 指令集,将汇编代码翻译成对应的二进制指令,同时生成一些必要的信息,以 section 的形式组装到目标文件中,后面的链接过程会用到这些信息,汇编的流程主要包括词法分析、语法分析、指令生成等过程。

编译器在编译一个项目时,是以 C 源文件为单位进行编译的,每一个源文件经过编译,生成一个对应的目标文件(main.c --> main.o)。 main.o 目标文件是不可执行的,属于可重定位的目标文件,要经过链接器重定位、链接后才能组装成一个可执行的目标文件 a.out。
在这里插入图片描述
通过编译生成的可重定位目标文件,都是以零地址为链接起始地址进行链接的。编译器在将源文件翻译成可重定位目标文件的过程中,将不同的函数编译成二进制指令后,是从零地址开始依次将每一个函数的指令序列存放到代码段中,每个函数的入口地址也就从零地址开始依次往后偏移。

仅仅汇编的命令:arm-linux-gnueabi-gcc -c main.c

使用 readelf 命令查看 main.o 目标文件:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -S main.o 
There are 12 section headers, starting at offset 0x354:Section Headers:[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al[ 0]                   NULL            00000000 000000 000000 00      0   0  0[ 1] .text             PROGBITS        00000000 000034 00005c 00  AX  0   0  4[ 2] .rel.text         REL             00000000 0002c0 000030 08   I  9   1  4[ 3] .data             PROGBITS        00000000 000090 000008 00  WA  0   0  4[ 4] .bss              NOBITS          00000000 000098 000004 00  WA  0   0  4[ 5] .rodata           PROGBITS        00000000 000098 000010 00   A  0   0  4[ 6] .comment          PROGBITS        00000000 0000a8 00002c 01  MS  0   0  1[ 7] .note.GNU-stack   PROGBITS        00000000 0000d4 000000 00      0   0  1[ 8] .ARM.attributes   ARM_ATTRIBUTES  00000000 0000d4 00002a 00      0   0  1[ 9] .symtab           SYMTAB          00000000 000100 000160 10     10  16  4[10] .strtab           STRTAB          00000000 000260 00005d 00      0   0  1[11] .shstrtab         STRTAB          00000000 0002f0 000061 00      0   0  1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),y (purecode), p (processor specific)

main.o 目标文件在编译时,都是以零地址为基地址进行代码段的组装。在每个可重定位目标文件中,函数或变量的地址起始就是它们在文件中相对于零地址的偏移。在后面的链接过程中,链接器在将各个目标文件组装在一起时,各个目标文件的参考地址就发生了变化,那么这个目标文件内的函数或变量的地址也要随之更新,否则就无法通过函数名去引用函数,无法通过变量名去引用变量。

链接器将各个目标文件组装在一起后,需要重新修改各个目标文件中的变量或函数的地址,这个过程一般称为重定位。将需要重定位的符号(函数名/变量名)收集生成重定位表,以 section 的形式保存到每个可重定位目标文件中就可以了。

main.o 中的 main 函数引用了 sub.o 中的 add 和 sub 函数。在链接器组装的过程中,add 和 sub 函数的地址发生了变化;在链接器组装之后,需要重新计算和更新 add 和 sub 函数的新地址,这个过程就是重定位。

符号表与重定位表

符号表和重定位表是非常重要的两个表,这两个表为链接过程提供各种必要的信息。

符号表

在汇编阶段,汇编器会分析汇编语言中各个 section 的信息,收集各种符号,生成符号表,将各个符号在 section 内的偏移地址也填充到符号表内。符号表主要用来保存源程序中的各种符号的信息,包括符号的地址、类型、占用空间的大小等。这些信息一方面可以辅助编译器作语义检查,看源程序是否有语义错误;另一方面也可以辅助编译器编译代码的生成,包括地址与空间的分配、符号决议、重定位等。

查看符号表:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -s main.oSymbol table '.symtab' contains 22 entries:Num:    Value  Size Type    Bind   Vis      Ndx Name0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c2: 00000000     0 SECTION LOCAL  DEFAULT    1 3: 00000000     0 SECTION LOCAL  DEFAULT    3 4: 00000000     0 SECTION LOCAL  DEFAULT    4 5: 00000000     0 NOTYPE  LOCAL  DEFAULT    3 $d6: 00000000     0 SECTION LOCAL  DEFAULT    5 7: 00000000     0 NOTYPE  LOCAL  DEFAULT    5 $d8: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a9: 00000054     0 NOTYPE  LOCAL  DEFAULT    1 $d10: 00000000     4 OBJECT  LOCAL  DEFAULT    4 uninit_local_val.461211: 00000000     0 NOTYPE  LOCAL  DEFAULT    4 $d12: 00000004     4 OBJECT  LOCAL  DEFAULT    3 local_val.461113: 00000000     0 SECTION LOCAL  DEFAULT    7 14: 00000000     0 SECTION LOCAL  DEFAULT    6 15: 00000000     0 SECTION LOCAL  DEFAULT    8 16: 00000000     4 OBJECT  GLOBAL DEFAULT    3 global_val17: 00000004     4 OBJECT  GLOBAL DEFAULT  COM uninit_val18: 00000000    92 FUNC    GLOBAL DEFAULT    1 main19: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND add20: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND sub21: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

符号表中的每一个符号,都有符号值和类型。符号值本质上是一个地址,可以是绝对地址,一般出现在可执行目标文件中;也可以是一个相对地址,一般出现在可重定位目标文件中。符号的类型主要有以下几种:

  • OBJECT:对象类型,一般用来表示我们在程序中定义的变量。
  • FUNC:关联的是函数名或其他可引用的可执行代码。
  • FILE:该符号关联的是当前目标文件的名称。
  • SECTION:表明该符号关联的是一个 section,主要用来重定位。
  • COMMON:表明该符号是一个公用块数据对象,是一个全局弱符号,在当前文件中未分配空间。
  • TLS:表明该符号对应的变量存储在线程局部存储中。
  • NOTYPE:未指定类型,或者目前还不知道该符号类型。

如果在一个 C 源文件中,我们引用了在其他文件中定义的函数或全局变量,编译器也不会报错,只需要在调用之前声明即可,编译器就会认为你引用的这个全局变量或函数可能在其他文件、库中定义,在编译阶段暂时不会报错。在后面的链接过程中,链接器会尝试在其他文件或库中查找你引用的这个符号的定义,如果真的找不到才会报错。此时的错误类型是链接错误。

重定位表

编译器在给每个目标文件生成符号表的过程中,如果在当前文件中没有找到符号的定义,也会将这些符号收集在一起并保存到一个单独的符号表中,以待后续填充,这个符号表就是重定位符号表。如在 main.o 的符号表(.symtab)中看到的:

    19: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND add20: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND sub21: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

add 和 sub 这两个符号的信息处于未定义状态(NOTYPE),需要后续填充。在 main.o 中会使用一个重定位表 .rel.text 记录这些需要重定位的符号:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -r main.oRelocation section '.rel.text' at offset 0x2c0 contains 6 entries:Offset     Info    Type            Sym.Value  Sym. Name
00000014  0000131c R_ARM_CALL        00000000   add
00000024  0000141c R_ARM_CALL        00000000   sub
00000034  0000151c R_ARM_CALL        00000000   printf
00000040  0000151c R_ARM_CALL        00000000   printf
00000054  00000602 R_ARM_ABS32       00000000   .rodata
00000058  00000602 R_ARM_ABS32       00000000   .rodata

在 .rel.text 中,我们可以看到需要重定位的符号 add、sub 以及库函数 printf,重定位表中的这些符号所关联的地址,在后面的链接过程中经过重定位,会更新为新的实际地址。

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

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

Mock-MOCO使用过程

一、jar包下载:https://github.com/dreamhead/moco 二、准备mock的json文件 data.json内容: ####GET请求 [{"description": "response使用Content-Type为charsetGBK编码格式来查看返回信息为中文的内容","request": {&q…

Linux 安装软件 - yum工具

在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序. 但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在一个服务器上, 通过包管理器可以很方便的获取到这个编译好的软件包, 直接进行…

Mac plist文件

macOS、iOS、iPadOS的应用程序都可能会有plist配置文件,他是苹果系列操作系统特有的配置文件。 plist的本质是个xml格式的文本文件,英文全称是property list,文件后缀使用.plist。 对于普通用户来说,基本不用管plist文件是什么&…

流星特效案例代码

实际效果&#xff0c;代码下载即可使用 流星图片 <!-- 描述: 流星特效 作者: Jack GUO 日期: 20230727 --> <template> <div class"wrap-container sn-container"> <div class"pd-main-left"> <div class"yun-container&…

最厉害的学生(c++题解)

题目描述 现有 N 名同学参加了期末考试&#xff0c;并且获得了每名同学的信息&#xff1a;姓名&#xff08;不超过 8 个字符的仅有英文小写字母的字符串&#xff09;、语文、数学、英语成绩&#xff08;均为不超过 150 的自然数&#xff09;。总分最高的学生就是最厉害的&…

全局ip代理安全吗? 手机设置全局代理方法详解

全局IP代理并不一定是安全的&#xff0c;因为全局IP代理会将所有网络流量都通过代理服务器进行转发&#xff0c;包括敏感信息和隐私数据。如果代理服务器受到黑客攻击或存在安全漏洞&#xff0c;可能会导致数据泄露和其他安全问题。因此&#xff0c;在使用全局IP代理时&#xf…

Mybatis增强框架Mybatis-Flex

一、Mybatis-Flex是什么&#xff1f; Mybatis-Flex 是一个优雅的 Mybatis 增强框架&#xff0c;它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库&#xff0c;其内置的 QueryWrapper^亮点 帮助我们极大的减少了 SQL 编写的工作的同时…

【Golang 接口自动化04】 解析接口返回JSON串

目录 前言 解析到结构体 json数据与struct字段是如何相匹配的呢&#xff1f; 解析到interface Go类型和JSON类型 实例代码 simpleJson 总结 资料获取方法 前言 上一次我们一起学习了如何解析接口返回的XML数据&#xff0c;这一次我们一起来学习JSON的解析方法。 JSO…
推荐文章