精华内容
下载资源
问答
  • 在编译过程中不是必要的
    千次阅读
    2021-02-02 02:13:11

    操作系统:ubantu
    工具gcc
    文件:test.c

    abner@DESKTOP-M6D2HEN:~/mytest$ cat test.c
    #include<stdio.h>
    
    #define MAXC 3
    
    #define MAXB MAXC + 1
    
    #define MAXA MAXB + 2
    
    int main()
    {
            int a = MAXA;
            return 0;
    }
    

    主要为四个阶段

    预处理->编译->汇编->连接

    C语言编译过程分成四个步骤:
    1,由.c文件到.i文件,这个过程叫预处理
    2,由.i文件到.s文件,这个过程叫编译
    3,由.s文件到.o文件,这个过程叫汇编
    4,由.o文件到可执行文件,这个过程叫链接

    当然一般不会单独生成.i .s中间文件。也不会一步步进行c代码的编译

    预处理

    (1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换
    (2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些
    (3)处理#include,将#include指向的文件插入到该行处,展开头文件
    (4)删除所有注释
    (5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
    (6)保留#pragma编译器指令:其他以#开拓的都是预编译指令,但是这个指令例外,此为编译器指示字,所以此步骤需要保留,关于此指示字的具体用法,在后面的内容将会详细讲解。

    预处理阶段其实就是将源文件进行完全展开,删除不必要的项,增加必要的项。

    abner@DESKTOP-M6D2HEN:~/mytest$ gcc -E test.c -o test.i
    abner@DESKTOP-M6D2HEN:~/mytest$ ll
    total 32
    drwxrwxrwx 1 abner abner  4096 Feb  2 01:58 ./
    drwxr-xr-x 1 abner abner  4096 Jan 31 23:07 ../
    -rw-r--r-- 1 abner abner   122 Jan 31 23:07 test.c
    -rw-r--r-- 1 abner abner 17929 Feb  2 01:58 test.i
    

    在这里插入图片描述

    编译

    编译过程一般包含:
    (1)词法分析
    (2)语法分析
    (3)语义分析
    (4)源代码优化
    (5)目标代码生成
    (6)目标代码优化

    编译就是将 高级语言 翻译为 汇编语言的过程。并且在该过程中相关优化代码。

    abner@DESKTOP-M6D2HEN:~/mytest$ gcc -S test.i -o test.s
    abner@DESKTOP-M6D2HEN:~/mytest$ ll
    total 32
    drwxrwxrwx 1 abner abner  4096 Feb  2 02:00 ./
    drwxr-xr-x 1 abner abner  4096 Jan 31 23:07 ../
    -rw-r--r-- 1 abner abner   122 Jan 31 23:07 test.c
    -rw-r--r-- 1 abner abner 17929 Feb  2 01:58 test.i
    -rw-r--r-- 1 abner abner   386 Feb  2 02:01 test.s
    

    在这里插入图片描述

    汇编

    汇编将 汇编语言 转变成 机器语言,生成目标文件
    每一个汇编语句几乎都对应一条机器指令。根据汇编指令和机器指令的对照表一一翻译即可。

    目标文件由段组成。通常一个目标文件中至少有两个段:
    代码段  该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
    数据段  主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    abner@DESKTOP-M6D2HEN:~/mytest$ gcc -c test.s -o test.o
    abner@DESKTOP-M6D2HEN:~/mytest$ ll
    total 36
    drwxrwxrwx 1 abner abner  4096 Feb  2 02:02 ./
    drwxr-xr-x 1 abner abner  4096 Jan 31 23:07 ../
    -rw-r--r-- 1 abner abner   122 Jan 31 23:07 test.c
    -rw-r--r-- 1 abner abner 17929 Feb  2 01:58 test.i
    -rw-r--r-- 1 abner abner  1232 Feb  2 02:02 test.o
    -rw-r--r-- 1 abner abner   386 Feb  2 02:01 test.s
    

    在这里插入图片描述
    可以看到内存分布的一些信息,代码段,数据段,bss段。堆栈是在程序的运行过程中。

    连接

    最后的连接阶段,将所有的 目标文件 和 需要的库连接,生成可执行文件。
    链接分为静态链接和动态链接。

    abner@DESKTOP-M6D2HEN:~/mytest$ gcc test.o -o test
    abner@DESKTOP-M6D2HEN:~/mytest$ ll
    total 44
    drwxrwxrwx 1 abner abner  4096 Feb  2 02:07 ./
    drwxr-xr-x 1 abner abner  4096 Jan 31 23:07 ../
    -rwxr-xr-x 1 abner abner  8168 Feb  2 02:07 test*
    -rw-r--r-- 1 abner abner   122 Jan 31 23:07 test.c
    -rw-r--r-- 1 abner abner 17929 Feb  2 01:58 test.i
    -rw-r--r-- 1 abner abner  1232 Feb  2 02:02 test.o
    -rw-r--r-- 1 abner abner   386 Feb  2 02:01 test.s
    

    参考:
    https://blog.csdn.net/qq_26079093/article/details/93037468
    https://blog.csdn.net/s226916240/article/details/102975171
    https://blog.csdn.net/chengdanna/article/details/70767329
    http://www.ha97.com/2830.html

    更多相关内容
  • 一文读懂Java编译过程

    千次阅读 2020-12-05 22:04:35
    Java文件编译过程包括两个阶段,第一阶段是编译阶段编译成Java字节码的过程,有些书籍叫前端编译器,如Oracle的javac编译器;第二阶段是运行时,通过JVM的编译优化组件,对代码的部分代码编译成本地代码,即...

    一文读懂Java编译全过程

    语言处理器种类

    1. 编译器,如gcc、javac解释器。
    2. 如Ruby、Python等一些一些语言使用解析器来实现的。
    3. IDE,如Eclipse、NetBeans等。
    4. 代码分析器,如FindBugs等。
    5. 反编译器,如JD、Jad、Reflector.NET等。

    Java编译过程

    Java文件编译过程包括两个阶段,第一阶段是在编译阶段编译成Java字节码的过程,有些书籍中叫前端编译器,如Oracle的javac编译器;第二阶段是在运行时,通过JVM的编译优化组件,对代码中的部分代码编译成本地代码,即JIT编译,如HotSpot中的C1、C2编译器。JVM整个编译过如下图所示。

    在这里插入图片描述

    其中,编译状态有如下9种。

    //编译状态 
    public enum CompileState {
            INIT(0),//初始化
            PARSE(1),//解析
            ENTER(2),//处理符号表
            PROCESS(3),//核心处理
            ATTR(4),//符号解析
            FLOW(5),//流分析
            TRANSTYPES(6),//解泛型为非泛型等类型转换
            UNLAMBDA(7),//解LAMBDA表达式
            LOWER(8),//解语法糖
            GENERATE(9);//生成字节码
     }
    

    下面是JIT编译器和C1(C2)编译器编译流程。
    在这里插入图片描述

    Javac编译器

    当我们在控制台执行javac命令时,找到javac对应的环境变量的可执行文件,通过JNI方式调用com.sun.tools.javac.Main.java中的main方法进入。也就是说Javac编译工作是由Java代码完成的。像javap,javah等命令也都是通过Java代码完成的。

       /**
         * launcher的入口.
         * Note: 该方法调用了System.exit.
         * @param args 命令行参数
         */
        public static void main(String[] args) throws Exception {
            System.exit(compile(args));
        }
    
         //此代码段在Main#compile方法中,用于读取Java文件对象用于编译。
          if (!files.isEmpty()) {
                    // add filenames to fileObjects
                    comp = JavaCompiler.instance(context);
                    List<JavaFileObject> otherFiles = List.nil();
                    JavacFileManager dfm = (JavacFileManager)fileManager;
                    for (JavaFileObject fo : dfm.getJavaFileObjectsFromFiles(files))
                        otherFiles = otherFiles.prepend(fo);
                    for (JavaFileObject fo : otherFiles)
                        fileObjects = fileObjects.prepend(fo);
                }
                //调用JavaCompiler#compile方法
                comp.compile(fileObjects,//要编译的文件对象
                             classnames.toList(),//注解处理的类名
                             processors);//用户提供的注解处理器
    

    最终调用JavaCompiler.compile()方法进行编译处理。如果自行编译,可以调用java中提供的工具类ToolProvider.getSystemJavaCompiler() 自行进行编译。如下是JavaCompiler.compiler()方法。

       /**
         * 主方法:要编译的文件列表,返回所有编译的类
         * @param sourceFileObjects 要编译的文件对象
         * @param classnames 为类中注解处理的类名
         * @param processors 用户提供的注解处理器,null意味着没有处理器提供。
         */
        public void compile(List<JavaFileObject> sourceFileObjects,
                            List<String> classnames,
                            Iterable<? extends Processor> processors)
        {
            if (processors != null && processors.iterator().hasNext())
                explicitAnnotationProcessingRequested = true;
            // 由于JavaCompiler只能使用一次,如果以前使用过,则抛出异常
            if (hasBeenUsed)
                throw new AssertionError("attempt to reuse JavaCompiler");
            hasBeenUsed = true;
    
            // forcibly set the equivalent of -Xlint:-options, so that no further
            // warnings about command line options are generated from this point on
            options.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");
            options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);
    
            start_msec = now();
    
            try {
                //检查是否要处理注解
                initProcessAnnotations(processors);
    
                // (1)这些方法必须是链式调用以避免内存泄漏
                delegateCompiler =
                    processAnnotations(
                        enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                        classnames);
                // (2)分析和生成字节码
                delegateCompiler.compile2();
                delegateCompiler.close();
                elapsed_msec = delegateCompiler.elapsed_msec;
            } catch (Abort ex) {
                if (devVerbose)
                    ex.printStackTrace(System.err);
            } finally {
                if (procEnvImpl != null)
                    procEnvImpl.close();
            }
        }
    

    从上面的代码可知,编译真正处理的代码在(1)和(2)处。对代码分析,编译处理包括以下三个部分。分别为解析与填充符号表、注解处理、分析和生成字节码三个大阶段。

    在这里插入图片描述

    解析与填充符号表

    解析与填充符号表,对应图一的词法分析、语法分析、抽象语法树、填充符合表几个细节处理。在解释语法树之前,我们首先要说下什么是语法树,语法树在很多语言中都有采用,如java、sql源码阅读中都用到了语法树的概念。如下的英语句子的语法树。
    在这里插入图片描述

    根据上面源码中的(1)注解中的代码,解析与填充符号表包括以下几个步骤。

    delegateCompiler =
                    processAnnotations(
                        enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                        classnames);
    
    1. parseFiles方法会执行parserFactory#newParser方法,在该方法内部封装了Scanner类并借助于JavaTokenizer类实现词法分析(手写的ad-hoc方式构造的词法分析器),词法分析可以简单理解为java文件中的每个空格之间的字符当作一个标记。源文件经过Unicode转义处理,通过Scanner类转化为令牌流。Parser类读取令牌流,使用TreeMaker创建语法树。而语法树通过com.sun.source.Tree及其子类的JCTree类或其子类实现的。语法树可理解为JCTree中每个节点表示一个包、类型等语法结构。每个树最后传递给Enter类,为遇到的所有定义的符号传入符号字面量。这必须在解析树之前做好,因为可能引用这些符号。该阶段输出的是“待办”列表,其中包含需要分析并生成文件的树。而Parser#parseCompilationUnit方法用于语法分析。
    2. enterTrees方法主要用于填充符号表。主要由Enter类实现。Enter包含很多阶段,要编译的类通过队列从一个阶段传到下一个阶段。

    在这里插入图片描述

    • 在第一个阶段,所有的类符号都进入到Enter的范围之内,树中其他类的成员变量都严格降序排列。类符号被赋予一个MemberEnter对象作为"完成者"。除此之外,如果任何package-info.java文件被找到,并且包含包注解。树节点的顶层将会为该文件添加到“代办”列表。

    • 将符号输入到符号表。com.sun.tools.javac.comp.Enter,每个编译单元的抽象语法树的顶局节点都先被放到待处理列表中,逐个处理列表中的节点,所有类符号被输入到外围作用域的符号表中,若找到package-info.java,将其顶局树节点加入到待处理列表中,确定类的参数(对泛型类型而言)、超类型和接口,根据需要添加默认构造器,将类中出现的符号输入到类自身的符号表中,分析和校验代码中的注解(annotation)。
      添加的默认构造器如下。
      在这里插入图片描述

    • 在第二阶段,类使用MemberEnter.complete()来完成。类是按需完成的,但是未按照此方式完成的类最终都会通过处理未完成的队列来完成。完成需要:(1)决定类的变量、超类和接口。(2)将类中定义的所有符号输入,但是在第一阶段已经完成的符号变量除外。(2)依赖于(1)中的类及其所有超类和封闭类已经完成。这就是为什么在(1)之后,我们将类放入到一个半完成的队列中。只有当我们对一个类及其所有超类和内部类执行了(1)之后,我们才继续执行(2)。

    • 输入所有的符号后,在这些符号上遇到的所有注解将会分析和验证。

      第一阶段是组织被遍历所有编译的语法树,而第二阶段是按需的,类的成员在第一次访问类的内容时输入,这是通过在编译类的类符号中使用completer对象来实现的,编译类调用对应类的树的MemberEnter阶段。

    注解处理

    注解是JDK1.5中引入的,对于注解的处理可以理解为编译器的一组插件,根据注解解析结果对抽象语法树进行修改,如lombok。方法processAnnotations是注解处理的入口,当由注解需要处理时,则由JavacProcessingEnvironment#doProcessing方法创建一个JavaCompiler对象来完成。从概念上来讲,注解处理是编译之前的一个初步步骤。这个初步动作由一系列的循环组成(如图2)。每个循环用于解析和输入源文件,然后确定和调用适当的注解处理器。在首次循环之后,如果被调用的任何注解处理器生成任何需要作为最后编译一部分的新原文件或类时,将需要执行后面的循环。最后,当所有必要的循环完成,执行实际编译。

    在这里插入图片描述

    在实际中,调用任何注解处理器的需要可能要等到要编译的文件被解析并且包含的声明被确定之后才能知道。因此,为了避免在不执行注解处理的情况下不必要地解析和输入源文件,JavacProcessingEnvironment对概念模型的执行有点不同,但是仍满足注解处理器作为一个整体在实际编译前执行。

    在这里插入图片描述

    当class文件被编译,并且已经解析和填充符号后。JavacProcessingEnvironment将会被调用。该类决定被编译的文件哪些注解需要被加载或被调用。通常,如果在整个编译过程中出现任何错误,该过程则在下一个合适的点停止编译。但是,如果在符号解析阶段出现丢失符号,则会抛出异常,因为定义这些符号可能作为注解处理器的结果。

    如果要运行注释处理器,将在单独的类加载器中加载并运行它们。

    当注解处理器运行时,JavacProcessingEnvironment决定是否需要另外一轮注解处理。如果需要,将会创建一个新的对象JavaCompiler。读取上步骤新生成的源文件进行解析。并且重新使用之前的语法树进行解析。所有的这些树都被输入到这个新编译器实例的符号表中,并且根据需要调用注解处理器。然后重复直到所有的注解编译完成。

    最后,JavacProcessingEnvironment返回JavaCompiler对象用于编译剩下的部分。这个对象是用于解析和输入初始文件集的原始实例,或者是JavacProcessingEnvironment创建的用于开始最后一轮编译的最新实例。

    下面以lombok为例说明

    1. 注解处理之前。

    在这里插入图片描述
    2. 注解处理后
    在这里插入图片描述

    分析和生成字节码

    当命令行中指定的所有文件被解析并输入到编译器的符号表中,并且注解也已经处理,JavaCompiler能处理分析的语法树,以生成相应的class文件。由delegateCompiler.compile2()方法进入。

       /**
         * 注释处理之后的阶段:属性、解语法糖,最后是代码生成。
         */
        private void compile2() {
            try {
                switch (compilePolicy) {
                case ATTR_ONLY://只需解析数据的属性
                    attribute(todo);
                    break;
    
                case CHECK_ONLY://用于属性和解析树的流分析检查
                    flow(attribute(todo));
                    break;
    
                case SIMPLE://流分析、语法糖处理、生成字节码
                    generate(desugar(flow(attribute(todo))));
                    break;
    
                case BY_FILE: {
                        Queue<Queue<Env<AttrContext>>> q = todo.groupByFile();
                        while (!q.isEmpty() && !shouldStop(CompileState.ATTR)) {
                            generate(desugar(flow(attribute(q.remove()))));
                        }
                    }
                    break;
    
                case BY_TODO:
                    while (!todo.isEmpty())
                        generate(desugar(flow(attribute(todo.remove()))));
                    break;
    
                default:
                    Assert.error("unknown compile policy");
                }
            } catch (Abort ex) {
                if (devVerbose)
                    ex.printStackTrace(System.err);
            }
    
            if (verbose) {
                elapsed_msec = elapsed(start_msec);
                log.printVerbose("total", Long.toString(elapsed_msec));
            }
    
            reportDeferredDiagnostics();
    
            if (!log.hasDiagnosticListener()) {
                printCount("error", errorCount());
                printCount("warn", warningCount());
            }
        }
    

    当分析树时,可以找到对成功编译所需的类的引用,但是这些类没有显示指定用于编译。根据编译选项,将在源路径和类路径中搜索此类的类定义。如果能在类文件中找到定义,将自动分析、输入源文件并将其放到待办事项列表中。这些在Attr.SourceCompleter类中实现。

    分析树和生成类文件的工作由一系列的观察者来处理进入了编译器代办事项列表。这些观察者没有必要分步对所有的源文件处理。事实上,内存问题会使这极不可取。唯一的要求是,“代办”列表最终会被每一个观察者处理,除非编译因为错误而提前终止。

    1. Attr和Check

      顶层类是“Attribute",使用Attr,这意味着语法树中的名称、表达式和其他元素将被解析并与相对应的类型和符号相关联。这可以通过Attr类或Check类检查到许多语义错误。

    语法分析的一个步骤,将语法树中名字、表达式等元素不变量、方法、类型等联系到一起,检查变量使用前是否已声明,推导泛型方法的类型参数,检查类型匹配性,迕行常量折叠。

    下面举例说明。
    (1)标注前。
    在这里插入图片描述

    (2)标注后。
    在这里插入图片描述

    1. Flow

      如果到目前没有错误,将会使用Flow进行类的流分析。流分析用于检查变量的明确分配和不可到达语句。检查所有checked exception都被捕获或抛出;检查变量的确定性赋值(1)所有局部变量在使用前必项确定性赋值;(2)有返回值的方法必须确定性返回值;检查变量的确定性不重复赋值(1)为保证final的语义。

    2. TransTypes

      将泛型类型的类转变为TransTypes类(裸类型,普通的java类型),同时插入必要的类型转换代码。

      下面给个示例。
      (1)类型转换前。
      在这里插入图片描述
      (2)类型转化后。
      在这里插入图片描述

    3. Lower

      语法糖使用Lower类来处理,它重写语法树,通过替换等价、简单子树来消除特定类型的子树。这将会处理内部类和嵌套类,类字面量,断言,foreach循环等。对于每个被处理的类,Lower类返回已转变类及所有转变的嵌套类和内部类的树的列表。尽管Lower通常处理顶层类,但也处理package-info.java的顶层树。对于这种树,Lower类将创建合成类来包含包的任何注解。
      在这里插入图片描述

      削除if (false) { … }形式癿无用代码。满足下述所有条件的代码被认为是条件编译的无用代码◦if语句的条件表达式是Java语言规范定义的常量表达式◦并且常量表达式值为false则then块为无用代码;反之则else块为无用代码。

    示例

    (1)Lower前
    在这里插入图片描述
    (2)Lower后
    在这里插入图片描述

    1. Gen

      Gen类用于方法代码的编译,它创建包含字节码的Code属性,通过JVM实例来执行方法。如果该步骤成功,则编译后的类由ClassWriter类写出。

    一旦一个类作为类文件被写出来,它的许多语法树和生成的字节码就不再需要了。为了节省内存,对树的这些部分和符号的引用将为空,以允许垃圾收集器恢复内存。

    1. 将实例成员初始化器收集到构造器中成为();将静态成员初始化器收集为();
    2. 从抽象语法树生成字节码。(1)后序遍历语法树(如下);(2)进行最后的少量代码转换,如String的+被生成为StringBuilder操作;x++/x–在条件允许时被优化为++x/–x
    3. 从符号表生成Class文件◦生成Class文件的结构信息。生成元数据(包括常量池)
      在这里插入图片描述

    整个前端编译过程如下图所示。

    以上步骤已经生成了.class文件。在运行期间,编译器将会进一步优化,即JIT优化。

    JIT编译

    JIT是即时编译器(Just In Time Compiler)的缩写,Hotspot中有两个即时编译器,分别为Client Compiler(C1)和Server Compiler(C2),C1和C2都是将字节码编译成本地代码,区别可以理解为C1是局部优化,而C2可以理解为专门面向服务端的。JVM有三种运行模式,分别是解释(interpreted mode)、编译模式(compiled mode)和混合模式(mixed mode)三种模式。Java1.8中默认的解释器与其中一个JIT编译器直接配合的方式执行,即采用混合模式。用户可以通过参数"-Xint"强制虚拟机运行在解释模式,此时编译器不工作。当然也可以使用参数"-Xcomp"强制虚拟机运行于“编译模式”。这时优先采用编译方式执行,但在某些情况下,解释器不得不介入才能执行。

    编译条件

    编译优化的条件主要针对热点代码,而热点代码主要有两种情况:

    1. 多次被调用的方法
    2. 多次执行的循环体

    无论第一种情况还是第二种情况,都是以整个方法作为编译对象。第二种情况而不是以循环体作为编译对象。只是处理方式不同,因为第二种编译方式发生在方法执行体中,而在运行时表现为方法栈,通过替换方法栈中的部分代码为编译后的本地代码,即通过栈上替换(On Stack Replacement,OSR)的方式进行JIT编译。

    很显然,无论采用哪种方法,编译器都需要识别哪些代码为热点代码。目前热点代码探测的方式有两种。

    1. 基于采样的热点探测。虚拟机启动一个检测线程周期性检查各个线程的栈顶,如果发现某个方法经常在栈顶,则认为是"热点代码"。这种方式简单但是不能精确统计某个方法的热点,且容易受线程阻塞等外界因素影响,当线程阻塞时,某个方法就会一直处于栈顶,从而不能精确统计方法的执行次数。
    2. 基于计数器的热点探测。虚拟机会为每个方法建立计数器,如果该方法超过规定的次数则认为是热点代码。

    在HotSpot中采用的是第二种方式,且对同一个方法采用了两个计数器。一个是记录在某段时间内方法调用次数的计数器,当某段时间内不满足编译时,则次数会衰减一半,所以是某段时间内的相对次数。另一个是记录方法中的循环体的计数器(称为回边计数器),而这个计数器会一直往上增长,是绝对计数,当溢出时,则调整计数器的值为溢出状态。当该两个计数器超过默认的阈值,则发生JIT编译。下面表格是不同编译模式下的默认值。两个计数器都可以通过虚拟机参数进行设定。

    方法调用计数器回边计数器
    1500次13995次
    C210000次10700次

    编译过程

    默认情况下,当虚拟机中的编译线程编译完成后,才能替换到JIT编译请求。用户可以通过参数-XX:-BackgroundCompilation来禁止后台编译。

    C1编译优化主要在AdvancedThresholdPolicy.cpp文件中。

    编译优化技术

    公共子表达式消除、方法内联、逃逸分析

    参考

    http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html

    展开全文
  • 编译原理_编译过程概述

    千次阅读 2019-04-11 01:26:12
    文章目录概述词法分析语法分析语义分析中间代码生成代码优化目标代码生成符号表管理(表格管理)出错处理 概述 编译程序即是将高级语言书写的源程序翻译成与之等价的目标...以中间代码为分水岭的原因是把编译过程分解...

    概述

    编译程序即是将高级语言书写的源程序翻译成与之等价的目标程序(汇编语言或机器语言)。其工作可分为六个阶段,见下图:

    Compile Procedure

    对于编译的各个阶段,逻辑上可以划分为前端和后端两部分。前端包括词法分析到中间代码生成中各个阶段的工作,后端则是优化及目标代码生成的阶段。

    以中间代码为分水岭的原因是把编译过程分解为与机器有关和无关两个部分。这样对同一种程序语言只需要一个前端,只要针对不同的机器开发不同的后端即可,前后端结合形成编译器。当语言改动时也只涉及前端部分的维护。而对于不同的程序语言只要有不同的前端,然后与同一个后端结合就可以得到程序语言在某种机器上的编译器了。

    下面简单介绍一下每个步骤(模块)的目的和工作,注意,下文步骤中使用的X,Y,Z等符号,在词法分析后实际上会是一个内部标识符,之所以照用原文只是为了方便理解。

    词法分析

    源程序可以看做是多行的字符串,词法分析是编译过程的第一阶段,其任务是对源程序逐字符扫描,从中识别出一个个“单词”,“单词”又叫做符号,它是程序语言的基本语法单位,如关键字(保留字)、标识符、常数、运算符、分隔符等。

    词法分析程序输出的“单词”以二元组的形式输出,即其种类和值。词法分析过程依据语言的词法规则,即描述“单词”结构的规则。

    如:

    PASCAL源程序一则:

    VAR X,Y,Z:real;
    X:= Y+Z*60;
    

    词法分析阶段将语句分割成以下单词序列:

    类型类型
    1. 关键字VAR9. 分号;
    2. 标识符X10. 标识符X
    3. 逗号,11. 赋值号:=
    4. 标识符Y12. 标识符Y
    5. 逗号,13. 加号+
    6. 标识符Z14. 标识符Z
    7. 冒号:15. 乘号*
    8. 标准标识符real16. 整常数60
    17. 分号;

    语法分析

    在词法分析结果的基础上,语法分析是根据语言的规则将单词符号序列分解成语法单位,如“表达式”、“语句”、”程序“等。语法规则就是语法单位的构成规则,通过语法分析确定整个输入串能否构成一个语法上正确的程序。如果程序没有错误,语法分析后就能正确的构造出 语法树;否则就会指出语法错误,并给出诊断。

    词法分析和语法分析本质上都是在对源程序的结构进行分析。

    如:

    根据上面词法分析的结果可以构造出的语法树如下图:

    Syntax trees

    语义分析

    语义分析阶段主要分析语法结构的含义,检查源程序是否包含静态语义错误,并收集类型信息供代码生成阶段使用。只有语法和语义都正确的源程序才能翻译成正确的目标程序。

    语义分析的主要工作就是对类型进行分析和检查,一般类型检查包括两点:类型载体及在其上的运算。如,整除取余运算符只能对整数使用,如果运算对象是浮点数就认为是类型错误。

    在确定源程序语法和语义后,就可以对其进行翻译并给出源程序的内部表示。对于声明语句,要记录所遇到的符号信息,此阶段还负责填写校验 符号表,对于可执行语句则分析其结构合理性,并补充必要的步骤。

    如:

    对于上面的变量声明语句VAR X,Y,Z:real;应生成下面的符号表(real类型占4位)

    符号类型逻辑地址
    Xreal0
    Yreal4
    Zreal8

    对于上面变量赋值语句X:= Y+Z*60;生成的语法树经过语义分析后应如下图,其中增加了一个语义处理点 inttoreal,它的作用是将整型数转换为浮点数:

    Syntax tree after analyse

    中间代码生成

    该阶段是根据语义分析的结果生成中间代码,“中间”即意味着并非可执行的机器码,它是一种简单含义明确的记号系统,有若干种形式,但所有中间代码的共同特征均为与具体机器无关,类似于算法伪代码的作用。最常用的中间代码是一种与汇编高度类似的三地址码,采用四元式实现,其形式为:(运算符, 运算对象1, 运算对象2, 运算结果)

    如:

    对于上述提到的赋值语句X:= Y+Z*60;可根据语义分析的结果生成以下四元序列式:

    1. (inttoreal, 60, -, t1)
    2. (*, Z, t1, t2)
    3. (+, Y, t2, t3)
    4. (:=, t3, -, X)

    其中t1, t2, t3均为编译程序生成的临时变量,用于存放临时的结果。

    代码优化

    由于编译器将源程序翻译成中间代码的工作是按固定模式进行的,因此可以发现中间代码中往往在时间和空间上均有较大浪费。当要生成高效的目标代码则必须进行优化,优化可以在中间代码生成阶段进行,也可以在目标代码生成阶段执行。

    由于中间代码与具体机器无关,因此对中间代码的优化主要是对程序的控制流和数据流的分析之上

    如:

    对上述赋值语句X:= Y+Z*60;生成的四元序列式优化,可以发现60是已知的常数,将它转换为浮点数60.0也可以在编译时完成,没有必要生成一个四元式。同时,发现t3的作用只是将结果传递给X,同样也不需要生成一个四元式。因此可优化成如下等价四元序列式:

    1. (*, Z, 60.0, t1)
    2. (+, Y, t1, X)

    当然这只是很简单的优化,实际上的优化要复杂的多,会涉及公共子表达式的提取等更多技术。

    目标代码生成

    目标代码生成是编译器的最后一个阶段,这一阶段将中间代码转化为目标机器上的绝对指令代码、可重定位指令代码或汇编指令代码,这一阶段与具体机器密切相关。

    如:

    使用两个寄存器R1和R2将上述的四元序列式生成下面的目标代码:

    1. MOVF Z, R2
    2. MULF #60.0, R2
    3. MOVF Y, R1
    4. ADDF R2, R1
    5. MOVF R1, X

    符号表管理(表格管理)

    符号表的主要作用就是记录符号的信息,以辅助语义检查和代码生成。在编译过程中需要对符号表进行快速的查找插入、修改、删除等操作。

    符号表的建立一般始于词法分析阶段,但也可以始于词法分析和语义分析阶段。有时候符号表的使用还会伴随到目标代码的运行阶段。

    出错处理

    源程序不可避免的会出现错误,这些错误大致分为静态错误和动态错误。

    动态错误又称为动态语义错误,它们发生在程序运行时,例如变量为0时做除数、引用数组元素下标错误等。

    静态错误是编译阶段发现的程序错误,可分为语法错误和静态语义错误,如关键字拼写错误、标点符号错误、表达式缺少操作数、括号不匹配等问题就是语法错误,语义错误主要指语义分析阶段发现的运算符与运算对象类型不合法等错误。

    在编译发生错误时,编译过程应想办法能够绕过去,以便在一次编译中发现尽可能多的错误。

    展开全文
  • GCC 编译器的使用&编译过程

    千次阅读 2020-10-21 23:15:37
    Windows 下进行开发时,只需要点几个按钮即可编译,集成开发环境(比如 Visual studio)已经将各种编译工具的使用封装好了。Linux 下也有很优秀的集成开发工具,但是更多的时候是直接使用编译工具;即使使用集成...

    在线课堂:https://www.100ask.net/index(课程观看)
    论  坛:http://bbs.100ask.net/(学术答疑)
    开 发 板:https://100ask.taobao.com/ (淘宝)
         https://weidongshan.tmall.com/(天猫)
    交流群一:QQ群:869222007(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
    交流群二:QQ群:536785813(单片机-嵌入式)
    公 众 号:百问科技


    版本日期作者说明
    V12020韦东山技术文档

    视频观看地址:
    https://www.100ask.net/detail/p_5f338ae3e4b075dc42ad44a1/8

    全文下载:
    嵌入式Linux系统开发完全手册 第二版

    源文件需要经过编译才能生成可执行文件。在 Windows 下进行开发时,只需要点几个按钮即可编译,集成开发环境(比如 Visual studio)已经将各种编译工具的使用封装好了。Linux 下也有很优秀的集成开发工具,但是更多的时候是直接使用编译工具;即使使用集成开发工具,也需要掌握一些编译选项。

    PC 机上的编译工具链为 gcc、ld、objcopy、objdump 等,它们编译出来的程序在 x86 平台上运行。要编译出能在 ARM 平台上运行的程序,必须使用交叉编译工具 xxx-gcc、xxx-ld 等(不同版本的编译器的前缀不一样,比如 arm-linux-gcc),下面分别介绍。

    2.1 配套视频内容大纲

    2.1.1 GCC 编译过程(精简版)

    一个 C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等 4 步才能变成可执行文件。
    在这里插入图片描述
    在日常交流中通常使用“编译”统称这 4 个步骤,如果不是特指这 4 个步骤中的某一个,本教程也依惯例使用“编译”这个统称。

    cc1 main.c -o /tmp/ccXCx1YG.s
    as -o /tmp/ccZfdaDo.o /tmp/ccXCx1YG.s
    cc1 sub.c -o /tmp/ccXCx1YG.s
    as -o /tmp/ccn8Cjq6.o /tmp/ccXCx1YG.s
    collect2 -o test /tmp/ccZfdaDo.o /tmp/ccn8Cjq6.o ....
    

    2.1.2 常用编译选项

    在学习时,我们暂时只需要了解下表中的选项。

    常用选项描述
    -E预处理,开发过程中想快速确定某个宏可以使用“-E -dM”
    -c把预处理、编译、汇编都做了,但是不链接
    -o指定输出文件
    -I指定头文件目录
    -L指定链接时库文件目录
    -l指定链接哪一个库文件

    2.1.3 怎么编译多个文件

    ① 一起编译、链接:

    gcc -o test main.c sub.c
    

    ② 分开编译,统一链接:

    gcc -c -o main.o main.c
    gcc -c -o sub.o sub.c
    gcc -o test main.o sub.o
    

    2.1.4 制作、使用动态库

    制作、编译:

    gcc -c -o main.o main.c
    gcc -c -o sub.o sub.c
    gcc -shared -o libsub.so sub.o (可以使用多个.o 生成动态库) 
    gcc -o test main.o -lsub -L /libsub.so/所在目录/
    

    运行:
    ① 先把 libusb.so 放到 PC 或板子上的/lib 目录,然后就可以运行 test 程序。
    ② 如果不想把 libusb.so 放到/lib,也可以放在某个目录比如/a,然后如下执行:

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a 
    ./test
    

    2.1.5 制作、使用静态库

    gcc -c -o main.o main.c
    gcc -c -o sub.o sub.c
    ar crs libsub.a sub.o (可以使用多个.o 生成静态库)
    gcc -o test main.o libsub.a (如果.a 不在当前目录下,需要指定它的绝对或相对路径)
    

    运行:
    不需要把静态库 libsub.a 放到板子上。
    注意:执行 arm-linux-gnueabihf-gcc -c -o sub.o sub.c 交叉编译需要在最后面加上 -fPIC 参数。

    2.1.6 很有用的选项

    gcc -E main.c // 查看预处理结果,比如头文件是哪个
    gcc -E -dM main.c > 1.txt // 把所有的宏展开,存在 1.txt 里 
    gcc -Wp,-MD,abc.dep -c -o main.o main.c // 生成依赖文件 abc.dep,后面 Makefile 会用
    

    我们的视频会配套写成书:《嵌入式 Linux 应用开发完全手册 升级版》。下面的资料会写进书里,我会写得详细一点。

    下面的资料来自 GCC 官方文档及一些中文资料,没必要逐一研读,用到时再翻翻。

    2.2 GCC 编译过程

    一个 C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等 4 步才能变成可执行文件。
    在这里插入图片描述
    在日常交流中通常使用“编译”统称这 4 个步骤,如果不是特指这 4 个步骤中的某一个,本教程也依惯例使用“编译”这个统称。

    本节文档使用 x86 上的 gcc 来试验,使用 ARM 板的交叉编译工具链做实验时效果也是类似的。不同的交叉编译器工具链前缀可能不同,比如 arm-linux-gcc。

    (1)预处理
    C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。

    (2)编译
    编译就是把 C/C++代码(比如上述的“.i”文件)“翻译”成汇编代码,所用到的工具为 cc1(它的名字就是 cc1,x86 有自己的 cc1 命令,ARM 板也有自己的 cc1 命令)。

    (3)汇编汇编就是将第二步输出的汇编代码翻译成符合一定格式的机器代码,在 Linux 系统上一般表现为 ELF目标文件(OBJ 文件),用到的工具为 as。x86 有自己的 as 命令,ARM 版也有自己的 as 命令,也可能是 xxxxas(比如 arm-linux-as)。“反汇编”是指将机器代码转换为汇编代码,这在调试程序时常常用到。

    (4)链接
    链接就是将上步生成的 OBJ 文件和系统库的 OBJ 文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为 ld 或 collect2。

    编译程序时,加上-v 选项就可以看到这几个步骤。比如:

    gcc -o hello hello.c -v 
    

    可以看到很多输出结果,我们把其中的主要信息摘出来:

    cc1 hello.c -o /tmp/cctETob7.s
    as -o /tmp/ccvv2KbL.o /tmp/cctETob7.s
    collect2 -o hello crt1.o crti.o crtbegin.o /tmp/ccvv2KbL.o crtend.o crtn.o
    

    以上 3 个命令分别对应于编译步骤中的预处理+编译、汇编和链接,ld 被 collect2 调用来链接程序。
    预处理和编译被放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:

    cpp -o hello.i hello.c
    cc1 hello.i -o /tmp/cctETob7.s
    

    我们不需要手工去执行 cpp、cc1、collect2 等命令,我们直接执行 gcc 并指定不同的参数就可以了。

    可以通过各种选项来控制 gcc 的动作,下面介绍一些常用的选项。

    常用选项描述
    -E预处理,开发过程中想快速确定某个宏可以使用“-E -dM”
    -c把预处理、编译、汇编都做了,但是不链接
    -o指定输出文件
    -I指定头文件目录
    -L指定链接时库文件目录
    -l指定链接哪一个库文件

    2.3 GCC 总体选项(Overall Option)

    (1)-c
    预处理、编译和汇编源文件,但是不作链接,编译器根据源文件生成 OBJ 文件。缺省情况下,GCC 通过用.o’替换源文件名的后缀.c’,.i’,`.s’等,产生 OBJ 文件名。可以使用-o 选项选择其他名字。GCC 忽 略-c 选项后面任何无法识别的输入文件。

    (2)-S
    编译后即停止,不进行汇编。对于每个输入的非汇编语言文件,输出结果是汇编语言文件。缺省情况下,GCC 通过用.s’替换源文件名后缀.c’,`.i’等等,产生汇编文件名。可以使用-o 选项选择其他名字。GCC
    忽略任何不需要汇编的输入文件。

    (3)-E
    预处理后即停止,不进行编译。预处理后的代码送往标准输出。

    (4)-o file
    指定输出文件为 file。无论是预处理、编译、汇编还是链接,这个选项都可以使用。如果没有使用-o’选项,默认的输出结果是:可执行文件为a.out’;修改输入文件的名称是source.suffix’,则它的 OBJ文件是‘source.o’,汇编文件是 `source.s’,而预处理后的 C 源代码送往标准输出。

    (5)-v
    显示制作 GCC 工具自身时的配置命令;同时显示编译器驱动程序、预处理器、编译器的版本号。以一个程序为例,它包含三个文件,代码在 02_options 目录下。下面列出源码:

    File: main.c
    01 #include <stdio.h>
    02 #include "sub.h"
    03 
    04 int main(int argc, char *argv[])
    05 {
    06 int i;
    07 printf("Main fun!\n");
    08 sub_fun();
    09 return 0;
    10 }
    11 
    File: sub.h
    01 void sub_fun(void);
    02 
    File: sub.c
    01 void sub_fun(void)
    02 {
    03 printf("Sub fun!\n");
    04 }
    05
    

    ARM 版本的编译工具与 gcc、ld 等工具的使用方法相似,很多选项是一样的。本节使用 gcc、ld 等工具进行编译、链接,这样可以在 PC 上直接看到运行结果。使用上面介绍的选项进行编译,命令如下:

    $ gcc -c -o main.o main.c
    $ gcc -c -o sub.o sub.c
    $ gcc -o test main.o sub.o
    

    其中,main.o、sub.o 是经过了预处理、编译、汇编后生成的 OBJ 文件,它们还没有被链接成可执行文件;最后一步将它们链接成可执行文件 test,可以直接运行以下命令:

    $ ./test
    Main fun!
    Sub fun!
    

    现在试试其他选项,以下命令生成的 main.s 是 main.c 的汇编语言文件:

    $ gcc -S -o main.s main.c
    

    以下命令对 main.c 进行预处理,并将得到的结果打印出来。里面扩展了所有包含的文件、所有定义的宏。在编写程序时,有时候查找某个宏定义是非常繁琐的事,可以使用`-dM –E’选项来查看。命令如下:

    $ gcc -E main.c
    

    2.4 警告选项(Warning Option)

    (1)-Wall
    这个选项基本打开了所有需要注意的警告信息,比如没有指定类型的声明、在声明之前就使用的函数、局部变量除了声明就没再使用等。

    上面的 main.c 文件中,第 6 行定义的变量 i 没有被使用,但是使用“gcc –c –o main.o main.c”进行编译时并没有出现提示。

    可以加上-Wall 选项,例子如下:

    $ gcc -Wall -c main.c
    

    执行上述命令后,得到如下警告信息:

    main.c: In function `main':
    main.c:6: warning: unused variable `i'
    

    这个警告虽然对程序没有坏的影响,但是有些警告需要加以关注,比如类型匹配的警告等。

    2.5 调试选项(Debugging Option)

    (1)-g
    以操作系统的本地格式(stabs,COFF,XCOFF,或 DWARF)产生调试信息,GDB 能够使用这些调试信息。在大多数使用 stabs 格式的系统上,’-g’选项加入只有 GDB 才使用的额外调试信息。可以使用下面的选项来生成额外的信息:‘-gstabs+’,’-gstabs’,‘-gxcoff+’,’、’-gxcoff’,‘-gdwarf+‘或`-gdwarf’,具体用法请读者参考 GCC 手册。

    2.6 优化选项(Optimization Option)

    (1)-O 或-O1
    优化:对于大函数,优化编译的过程将占用稍微多的时间和相当大的内存。不使用-O'或-O1’选项的目的是减少编译的开销,使编译结果能够调试、语句是独立的:如果在两条语句之间用断点中止程序,可以对任何变量重新赋值,或者在函数体内把程序计数器指到其他语句,以及从源程序中精确地获取你所期待的结果。

    不使用‘-O’或’-O1’选项时,只有声明了 register 的变量才分配使用寄存器。

    使用了’-O’或‘-O1’选项,编译器会试图减少目标码的大小和执行时间。如果指定了‘-O’或’-O1’选项,, ‘-fthread-jumps’和’-fdefer-pop’选项将被打开。在有 delay slot 的机器上,‘-fdelayed-branch’选项将被打开。在即使没有帧指针 (frame pointer)也支持调试的机器上,`-fomit-frame-pointer’选项将被打开。某些机器上还可能会打开其他选项。

    (2)-O2
    多优化一些。除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作。例如不进行循环展开(loop unrolling)和函数内嵌(inlining)。和’-O’或`-O1’选项比较,这个选项既增加了编译时间,也提高了生成代码的运行效果。

    (3)-O3
    优化的更多。除了打开-O2 所做的一切,它还打开了-finline-functions 选项。

    (4)-O0
    不优化。

    如果指定了多个-O 选项,不管带不带数字,生效的是最后一个选项。
    在一般应用中,经常使用-O2 选项,比如对于 options 程序:

    $ gcc -O2 -c -o main.o main.c
    $ gcc -O2 -c -o sub.o sub.c
    $ gcc -o test main.o sub.o
    

    2.7 链接器选项(Linker Option)

    下面的选项用于链接 OBJ 文件,输出可执行文件或库文件。

    (1)object-file-name
    如果某些文件没有特别明确的后缀(a special recognized suffix),GCC 就认为他们是 OBJ 文件或库文件(根据文件内容,链接器能够区分 OBJ 文件和库文件)。如果 GCC 执行链接操作,这些 OBJ 文件将成为链接器的输入文件。

    比如上面的“gcc -o test main.o sub.o”中,main.o、sub.o 就是输入的文件。

    (2)-llibrary
    链接名为 library 的库文件。

    链接器在标准搜索目录中寻找这个库文件,库文件的真正名字是‘liblibrary.a’。搜索目录除了一些系统标准目录外,还包括用户以’-L’选项指定的路径。一般说来用这个方法找到的文件是库文件──即由 OBJ文件组成的归档文件(archive file)。链接器处理归档文件的方法是:扫描归档文件,寻找某些成员,这些成员的符号目前已被引用,不过还没有被定义。但是,如果链接器找到普通的 OBJ 文件,而不是库文件,就把这个 OBJ 文件按平常方式链接进来。指定‘-l’选项和指定文件名的唯一区别是,’-l’选项用‘lib’和`.a’把 library 包裹起来,而且搜索一些目录。即使不明显地使用-llibrary 选项,一些默认的库也被链接进去,可以使用-v 选项看到这点:

    $ gcc -v -o test main.o sub.o
    

    输出的信息如下:

    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
    -o test 
    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/../../../crt1.o /usr/lib/gcc-lib/i386-redhatlinux/3.2.2/../../../crti.o 
    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/crtbegin.o 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/../../.. 
    main.o 
    sub.o 
    -lgcc -lgcc_eh -lc -lgcc -lgcc_eh 
    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/crtend.o 
    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/../../../crtn.o
    

    可以看见,除了 main.o、sub.o 两个文件外,还链接了启动文件 crt1.o、crti.o、crtend.o 、crtn.o,还有一些库文件(-lgcc -lgcc_eh -lc -lgcc -lgcc_eh)。

    (3)-nostartfiles
    不链接系统标准启动文件,而标准库文件仍然正常使用:

    $ gcc -v -nostartfiles -o test main.o sub.o
    

    输出的信息如下:

    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/collect2 --eh-frame-hdr -m elf_i386 -dynamiclinker 
    /lib/ld-linux.so.2 
    -o test 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/../../.. 
    main.o 
    sub.o 
    -lgcc -lgcc_eh -lc -lgcc -lgcc_eh
    /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 08048184
    

    可以看见启动文件 crt1.o、crti.o、crtend.o 、crtn.o 没有被链接进去。需要说明的是,对于一般应用程序,这些启动文件是必需的,这里仅是作为例子(这样编译出来的 test 文件无法执行)。在编译bootloader、内核时,将用到这个选项。

    (4)-nostdlib
    不链接系统标准启动文件和标准库文件,只把指定的文件传递给链接器。这个选项常用于编译内核、bootloader 等程序,它们不需要启动文件、标准库文件。

    仍以 options 程序作为例子:

    $ gcc -v -nostdlib -o test main.o sub.o
    

    输出的信息如下:

    /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/collect2 --eh-frame-hdr -m elf_i386 -dynamiclinker /lib/ld-linux.so.2 
    -o test 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2 
    -L/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/../../.. 
    main.o 
    sub.o
    /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 08048074
    main.o(.text+0x19): In function `main':
    : undefined reference to `printf'
    sub.o(.text+0xf): In function `sub_fun':
    : undefined reference to `printf'
    collect2: ld returned 1 exit status
    

    出现了一大堆错误,因为 printf 等函数是在库文件中实现的。在编译 bootloader、内核时,用到这个选项──它们用到的很多函数是自包含的。

    (5)-static
    在支持动态链接(dynamic linking)的系统上,阻止链接共享库。
    仍以 options 程序为例,是否使用-static 选项编译出来的可执行程序大小相差巨大:

    $ gcc -c -o main.c
    $ gcc -c -o sub.c
    $ gcc -o test main.o sub.o
    $ gcc -o test_static main.o sub.o –static
    $ ls -l test test_static
    -rwxr-xr-x 1 book book 6591 Jan 16 23:51 test
    -rwxr-xr-x 1 book book 546479 Jan 16 23:51 test_static
    

    其中 test 文件为 6591 字节,test_static 文件为 546479 字节。当不使用-static 编译文件时,程序执行前要链接共享库文件,所以还需要将共享库文件放入文件系统中。

    (6)-shared
    生成一个共享 OBJ 文件,它可以和其他 OBJ 文件链接产生可执行文件。只有部分系统支持该选项。当不想以源代码发布程序时,可以使用-shared 选项生成库文件,比如对于 options 程序,可以如下制作库文件:

    $ gcc -c -o sub.o sub.c
    $ gcc -shared -o libsub.so sub.o
    

    以后要使用 sub.c 中的函数 sub_fun 时,在链接程序时,指定引脚 libsub.so 即可,比如:

    $ gcc -o test main.o -lsub -L /libsub.so/所在的目录/
    

    可以将多个文件制作为一个库文件,比如:

    $ gcc -shared -o libsub.so sub.o sub2.o sub3.o
    

    (7)-Xlinker option
    把选项 option 传递给链接器。可以用来传递系统特定的链接选项,GCC 无法识别这些选项。如果需要传递携带参数的选项,必须使用两次’-Xlinker’,一次传递选项,另一次传递其参数。例如,如果传递‘-assert definitions’,要成’-Xlinker -assert -Xlinker definitions’,而不能写成`-Xlinker “-assert definitions”’,因为这样会把整个字符串当做一个参数传递,显然这不是链接器期待的。

    (8)-Wl,option
    把选项 option 传递给链接器。如果 option 中含有逗号,就在逗号处分割成多个选项。链接器通常是通过 gcc、arm-linux-gcc 等命令间接启动的,要向它传入参数时,参数前面加上`-Wl,’。

    (9)-u symbol
    使链接器认为取消了 symbol 的符号定义,从而链接库模块以取得定义。可以使用多个 `-u’选项,各自跟上不同的符号,使得链接器调入附加的库模块。

    2.8 目录选项(Directory Option)

    下列选项指定搜索路径,用于查找头文件,库文件,或编译器的某些成员。
    (1)-Idir
    在头文件的搜索路径列表中添加 dir 目录。
    头文件的搜索方法为:如果以“#include < >”包含文件,则只在标准库目录开始搜索(包括使用-Idir选项定义的目录);如果以“#include “ ””包含文件,则先从用户的工作目录开始搜索,再搜索标准库目录。

    (2)-I-
    任何在‘-I-'前面用‘-I’选项指定的搜索路径只适用于’#include “file”'这种情况;它们不能用来搜
    索’#include '包含的头文件。如果用‘-I’选项指定的搜索路径位于’-I-'选项后面,就可以在这些路径中搜索所有的‘#include’指令(一般说来-I 选项就是这么用的)。还有,’-I-'选项能够阻止当前目录(存放当前输入文件的地方)成为搜索‘#include “file”'的第一选择。’-I-'不影响使用系统标准目录,因此,‘-I-'和`-nostdinc’是不同的选项。

    (3)-Ldir
    在`-l’选项的搜索路径列表中添加 dir 目录。
    仍使用 options 程序进行说明,先制作库文件 libsub.a:

    $ gcc -c -o sub.o sub.c
    $ gcc -shared -o libsub.a sub.o
    

    编译 main.c:

    $ gcc -c -o main.o main.c
    

    链接程序,下面的指令将出错,提示找不到库文件:

    $ gcc -o test main.o -lsub
    /usr/bin/ld: cannot find -lsub
    collect2: ld returned 1 exit status
    

    可以使用-Ldir 选项将当前目录加入搜索路径,如下则链接成功:

    $ gcc -L. -o test main.o -lsub
    

    (4)-Bprefix
    这个选项指出在何处寻找可执行文件,库文件,以及编译器自己的数据文件。编译器驱动程序需要使用某些工具,比如:’cpp’,‘cc1’ (或 C++的’cc1plus’),‘as’和‘ld’。它把 prefix 当作欲执行的工具的前缀,这个前缀可以用来指定目录,也可以用来修改工具名字。

    对于要运行的工具,编译器驱动程序首先试着加上’-B’前缀(如果存在),如果没有找到文件,或没有指定‘-B’选项,编译器接着会试验两个标准前缀’/usr/lib/gcc/‘和’/usr/local/lib/gcc-lib/’。如果仍然没能够找到所需文件,编译器就在‘PATH’环境变量指定的路径中寻找没加任何前缀的文件名。如果有需要,运行时(run-time)支持文件’libgcc.a’也在‘-B’前缀的搜索范围之内。如果这里没有找到,就在上面提到的两个标准前缀中寻找,仅此而已。如果上述方法没有找到这个文件,就不链接它了。多数情况的多数机器上,’libgcc.a’并非必不可少。

    可以通过环境变量 GCC_EXEC_PREFIX 获得近似的效果;如果定义了这个变量,其值就和上面说的一样被用作前缀。如果同时指定了‘-B’选项和 GCC_EXEC_PREFIX 变量,编译器首先使用`-B’选项,然后才尝试环境变量值。

    2.9 ld/objdump/objcopy 选项

    我们在开发 APP 时,一般不需要直接调用这 3 个命令;在开发裸机、bootloader 时,或是调试 APP 时会涉及,到时再讲。

    -end-


    百问网技术论坛:
    http://bbs.100ask.net/

    百问网官方wiki(资料下载):
    http://wiki.100ask.org/

    线上课堂:
    https://www.100ask.net/index

    百问网开发板:
    淘宝:https://100ask.taobao.com/
    天猫:https://weidongshan.tmall.com/

    技术交流群(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
    QQ群:869222007

    单片机-嵌入式Linux交流群:
    QQ群:536785813
    百问科技

    百问科技公众号

    在这里插入图片描述

    百问科技服务号
    展开全文
  • 浅谈C#编译过程和原理

    千次阅读 2019-07-22 12:19:57
    最近研究学习C#的时候,研究了一下C#的运行原理和编译过程,多次查找各种文章和书籍总结后得出的结论,新手入门,只为总结记录,有问题的地方还望有人能看见指正 C#的特点 首先先说一下C#的一些特点: 第一,C#是...
  • 编译原理学习(一)--编译以及编译过程

    万次阅读 多人点赞 2018-05-22 21:01:00
    【龙书】编译原理(第二版)学习与理解:1.也许我们这辈子都不会去实现一个编译器,但是我们至少要知道编译器是什么?为什么会需要编译器? ①编译器首先也是一种电脑程序。它会将用某种编程语言写成的源代码(原始...
  • 完全新手教程:编译openwrt全过程

    千次阅读 2021-05-26 14:49:24
    此文www.openwrt.org.cn首发,个人不主动其它地方转发,没任何版权问题,有兴趣的网友随便复制转发,但机于对本论坛的尊重,应该注明原出处URL,但没必要强调是谁写的。 原文链接 我也搞不懂为什么,我想玩编译...
  • gcc编译过程

    千次阅读 2018-05-31 20:44:39
    Gcc的编译流程分为了四个步骤:  1.预处理,生成预编译文件(.i文件):  Gcc –E hello.c –o hello.i  2.编译,生成汇编代码(.s文件):  Gcc –S hello.i –o hello.s  3.汇编,生成目标文件(.o文件):...
  • ANDROID系统编译过程详解

    万次阅读 2019-06-17 16:08:23
    Make命令执行的时候,默认会当前目录找到一个Makefile文件,然后根据Makefile文件的指令来对代码进行编译。也就是说,make命令执行的是Makefile文件的指令。Makefile文件的指令可以是编译命令,...
  • 计算机程序编译原理学习心得

    千次阅读 2021-06-25 06:08:50
    计算机程序编译原理学习心得《编译原理》是计算机专业的一门重要课程,正如教材:第一章的引论所述,“编译程序是现代计算机系统的基本组成部分之一”。“一个编译程序就是一个语言翻译程序,语言翻译程序把一种语言...
  • linux内核编译过程的最终总结版

    万次阅读 多人点赞 2018-09-15 19:01:28
    Linux操作系统环境下重新编译内核。实验主要内容: A.查找并且下载一份内核源代码,本实验使用最新的Linux内核2.6.36。 B.配置内核。 C.编译内核和模块。 D.配置启动文件。 本次实验环境是Linux2.6.35内核的...
  • Linux-C C语言编译过程

    千次阅读 2018-07-18 20:32:55
    Linux-C C语言编译过程 一、简述  GCC(GNU Compiler Collection,即 GNU 编译器套装),是一套由 GNU 开发的编程 语言编译器。简单介绍使用gcc编译器将hello.c文件编译成为hello可执行文件的过程。 伪...
  • 交叉编译过程中遇到fatal error: seccomp.h或者ltdl.h: No such file or ...交叉编译过程中,出现fatal error: seccomp.h: No such file or directory compilation terminated. fatal error:ltdl.h: No such file or
  • glibc编译方法

    千次阅读 2021-11-07 21:48:04
    我们开发过程中,有时候可能需要根据我们的业务场景对glibc进行定制化修改,因此有必要了解glibc的编译方法。通常编译glibc需要以下几个步骤: 1. 由于我们一般是x86环境的编译服务器下编译运行arm环境下的...
  • 内核单个.o文件的编译过程

    千次阅读 2017-02-23 12:41:45
    kbuid系统,单个.o文件的编译过程
  • g++编译详解

    万次阅读 多人点赞 2019-12-24 18:50:20
    g++编译详解 资料准备: 为了方便演示和讲解,这里提前准备好几个简单的文件:test.cpp test.h main.cpp 文件内容如下: main.cpp //main.cpp int main (int argc, char **argv) { return 0; } test.h //test....
  • FPGA之道(18)FPGA设计的编译过程

    千次阅读 2020-02-11 01:37:06
    这里所谓的FPGA设计的实现过程不是说等价于Implementation,而是整个FPGA设计从设计的描述、编译到最终配置文件形成的一整套过程
  • 编译程序

    千次阅读 2020-10-23 09:15:55
    这一章,我们将看一下如何通过编译源代码来创建程序。源代码的可用性是至关重要的自由,从而使得 Linux 成为可能。 整个 Linux 开发生态圈就是依赖于开发者之间的自由交流。对于许多桌面用户来说,编译是一种...
  • 前面2.编译前需要准备的环境和工具。3.具体步骤4.总结 1.写前面 Spring作为风靡世界的优秀框架,很早就想研究研究Spring源码了。最近亲手搭建了Spring环境,从下载源码到编译到改动源码测试demo跑通,有些踩坑...
  • 交叉编译详解

    千次阅读 2021-02-23 13:31:29
    第 1 章 交叉编译简介 1.1 什么是交叉编译 对于没有做过嵌入式编程的人,可能不太理解交叉编译的...这种方式下,我们使用 x86 平台上的工具,开发针对 x86 平台本身的可执行程序,这个编译过程称为本地编译。 交叉
  • 编译原理】引论

    千次阅读 2020-02-18 20:59:54
    文章目录编译原理引论(一)认识编译程序(二)编译过程概述1、阶段划分2、编译程序的结构3、编译程序的生成 编译原理引论 (一)认识编译程序 什么是编译程序? 这要从翻译程序、解释程序以及编译程序的联系与区别...
  • Linux内核配置和编译过程详解

    万次阅读 2016-05-24 12:48:00
    还有每一个大项和文档的最后会有一个经验谈,它是一些高手们应对问题和处理特有硬件时的一些经验(这个还得靠各位)。 文档最后会发到网上,到时会根据网友们的回复随时进行更新。  我们的目的是让我们有一个
  • uboot之Makefile编译过程详解

    千次阅读 2016-12-15 16:30:24
    不是源码自带,是配置过程中( make x210_sd_config )生成的文件。因此,这个文件的值是和我们的配过程相关的。 当我们编译完成后, include 目录的 config.mk 文件有:   ARCH = arm...
  • 手把手,手摸手带你边编译,边发现问题,边解决问题
  • 1、参考书籍:《编译原理》第三版 清华大学出版社 1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正) 2、本文目的:开源共享 抛砖引玉 一起学习 3...
  • 1. 本节课内容大纲 2. 元张量函数 3. 张量程序抽象 3.1 为什么需要进行张量程序抽象 ...3.3 张量程序抽象的其它结构 4. 张量程序变换实践 4.1 安装包 4.2 构造张量程序 4.3 编译与运行 4.4 张量程序变换 5. 总结...
  • openwrt编译流程分析

    千次阅读 2021-02-18 16:18:57
    最近开始整5G CPE项目,系统基于OpenWrt,打算...虽然我们开始编译前已经安装了一些必要的工具,但编译过程中还需要其他一些主机工具。这部分工具将首先编译。 2. 编译交叉工具链 openwrt自带交叉编译链,当然
  • 如题!想了解编译必要性还有不编译会怎么样 --------------------------------------------------
  • 一、详解编译、链接 有些人写C/C++(以下假定为C++)程序,对unresolved external link或者duplicated external simbol的错误信息不知所措(因为这样的错误信息不能定位到某一行)。或者对语言的一些部分不知道为什么...
  • LLVM编译流程详解

    千次阅读 2020-06-27 22:40:36
    LLVM项目是一系列分模块、可重用的编译工具链。它提供了一种代码编写良好的中间表示(IR),可以作为多种语言的后端,还可以提供与变成语言无关的优化和针对多种cpu的代码生成功能。 传统编译器分为三个阶段:前端—&...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 309,445
精华内容 123,778
关键字:

在编译过程中不是必要的