C编译涉及哪些内部进程?

chy5wohz  于 2022-12-11  发布在  其他
关注(0)|答案(3)|浏览(117)

我有一组 *.C文件(嵌入式相关)。
在编译后进行链接以创建最终可执行文件时,涉及哪些步骤/过程(内部信息)?(有关预处理器/编译器通常对C src代码执行哪些操作的信息/步骤。)
最终可执行文件的一般结构是什么(例如:标头后面是符号表等)?

r7xajy2e

r7xajy2e1#

以gcc为例,我认为要使用的选项是-save-temps。
大致的步骤是在文件上做一个传递,把所有的包含都拉进来,并创建一个要解析的文件。现在很多工具使用一个解析器,它运行在一组规则上(野牛,yacc,flex等),目标是解析ascii,把你的程序变成一种非常广泛的汇编语言,因为缺乏一个更好的术语。

a = a + 1;

可能变成

Load variable named a, size of blah, type unsigned foo
load immediate 1, size blah, unsigned
add
store result a

最后,这些优化完成,并且该中间代码通过后端到达目标指令集。这通常作为汇编输出,并且被馈送到汇编器,汇编器将其转换为目标文件,和目标特定的优化可以发生。然后目标文件被馈送到链接器,链接器将它们链接在一起。一个程序中的一个函数可能正在调用一个不在名为bob的目标文件中的函数,对象文件没有到达Bob的地址或偏移量,它在那里留下了用于插入地址的孔,并且链接器的工作是连接所有这些,确定函数Bob将位于二进制文件中的何处(为其分配地址),然后找到调用Bob的所有位置,并且当这些位置被放置在存储器中时,插入允许Bob被调用所需的指令或地址,使得最终结果是可执行的二进制文件。
llvm已经是gcc的竞争对手,它提供了对这个过程的良好可见性。你可以将C代码编译成一个中间体。从我们的bob函数开始

unsigned int bob ( unsigned int a )
{
    return(a+1);
}

编译成位码

clang -c -o bob.bc -emit-llvm bob.c

将位代码反汇编为人类可读的形式

llvm-dis bob.bc

结果是bob.ll。

define i32 @bob(i32 %a) nounwind {
entry:
  %a.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  %tmp = load i32* %a.addr, align 4
  %add = add i32 %tmp, 1
  ret i32 %add
}

非优化代码喜欢经常被存储和从内存中提取,并且当传递到函数时经常被存储和从堆栈中提取。
除了能让你轻松地看到幕后的情况之外,llvm还很不错,因为你可以在任何级别进行优化,可以合并对象,可以在整个程序级别进行优化,而gcc只会将你限制在文件或函数级别。

opt -std-compile-opts bob.bc -o bob_opt.bc
llvm-dis bob_opt.bc

那些额外的存储和负载都消失了,功能的实质仍然存在。

define i32 @bob(i32 %a) nounwind readnone {
entry:
  %add = add i32 %a, 1
  ret i32 %add
}

然后使用llc将其转换为所需目标的汇编程序。

llc -march=arm bob.bc
cat bob.s
...
bob:                                    @ @bob
@ BB#0:                                 @ %entry
    str r0, [sp, #-4]!
    add r0, r0, #1
    add sp, sp, #4
    bx  lr
...
llc -march=arm bob_opt.bc
cat bob_opt.s
...
bob:                                    @ @bob
@ BB#0:                                 @ %entry
    add r0, r0, #1
    bx  lr
...

是的,有很多很多的书在那里。和很多很多编译器,等等。除了llvm,Fabrice Bellard(是的qemu的人),有一个超级简单,几乎没有编译器,产生一个中间文件,你可以检查http://bellard.org/fbcc/,这是埋葬,使它几乎不为人所知,有趣的看,虽然如果你只是进入编译器的内脏。此外,还有一个众所周知的,TCCX 1 E1 F1 X特别地,该TCCX 1 E1 F1 X不具有经过汇编器的后端,为了速度和真实的(重新)编译,直接生成操作码。

qacovj5a

qacovj5a2#

如果你真的想知道它是如何工作的,我建议你读A Retargetable C Compiler。它将介绍构建C编译器的所有步骤(我相信这本书涵盖了lcc编译器)。

f45qwnt8

f45qwnt83#

一般来说,编译器的输出是一个目标文件,其中包含了相应源文件中函数的可执行代码。(编译器可以直接生成机器码,也可以生成汇编语言,然后通过单独的汇编程序将其转换为机器码)。目标文件还包含其他内容,如静态数据对象、外部符号定义和引用。
链接器的工作是获取一组目标文件,并匹配符号引用/定义。例如,如果a. c定义了一个函数a,b. c调用了a(),链接器需要修补b的目标代码,以“填充”正确的a地址。(这是一种过度简化的做法,因为还有动态加载、共享库、重定位等。)
没有一个标准的可执行文件格式;一些常见的格式包括.EXE(32或64位)、ELF和Mach-O。
这个article in Linux Journal给出了链接器和加载器如何工作的更详细的解释。

相关问题