编译_编译器 - CSDN
精华内容
参与话题
  • 编译的基本概念

    千次阅读 2019-07-10 14:33:39
    CPU执行程序的原理这篇文章中提到,“程序要想被CPU执行,首先要被编译成CPU可以执行的指令操作”,那编译成CPU可以执行的指令操作是什么意思呢?这篇文章就用来说明编译的实际意义是什么。 2、知识背景——CPU架构...

    1、本文目的

    CPU执行程序的原理这篇文章中提到,“程序要想被CPU执行,首先要被编译成CPU可以执行的指令操作”,那编译成CPU可以执行的指令操作是什么意思呢?这篇文章就用来说明编译的实际意义是什么。

    2、知识背景——CPU架构

    要谈编译,首先要说说CPU架构的概念。CPU架构也就是CPU指令集(指令就是汇编指令或者机器指令,比如Add是汇编指令,而对应的机器指令在MIPS下就是000000)架构,现有CPU架构包括鼎鼎有名的Intel的X86架构、ARM的ARM架构、MIPS的MIPS架构、DEC的Alpha架构。通俗来说,指令集就是指挥CPU如何运算的硬程序,没有这套指令的话,就没有办法指挥CPU运转,而计算机的所有运算都需要CPU参与。

    那编译呢,也就是将一段程序转换为指令集的过程。不同架构的指令集自然是不同的,带来的影响就是同一段代码,编译过后只能运行在对应的指令集上,比如一段C++代码,在X86下编译完了,只能在X86下运行,而不能运行在ARM架构下运行。

    而事实上,编译得到的结果,更是操作系统相关的。假设,一段程序被编译成了X86下的硬程序,但是无法同时运行在Windows上和Linux上(Windows和Linux操作系统都可以装在X86架构的CPU上),如果程序一开始是在Windows操作系统下编译的,那这段程序就无法运行在其他比如Linux操作系统中。

    也就是说,编译与操作系统和CPU这二者都是相关的。

    3、编译过程

    事实上,仅仅将程序通过编译改写成汇编指令或机器指令,在操作系统上还不能直接运行。实际上广义的编译,其实包括预处理、编译、汇编、链接这整个过程。

    1. 预处理,就是把代码里引入的其他代码,插入到这段代码中,形成一个代码文件。
    2. 编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。
    3. 汇编,就是把汇编指令转为机器码的过程,机器码可以被CPU直接执行。
    4. 链接,就是将一段我们需要的已经编译好的其他库,与我们的汇编结果连起来,这样才是最终程序完整的形式,操作系统才可以运行。不同操作系统编译好的其他库形式不同,而且链接的方式也不同,得到最终程序的形式也不同,所以编译好的程序只能在特定的操作系统下运行。
    展开全文
  • 我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动。 预处理, 展开头文件/宏...

    楔子

    我们在各自的电脑上写下代码,得明白我们代码究竟是如何产生的,不想了解1,0什么的,但这几个环节必须掌握吧。

    我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动。

     

    预处理, 展开头文件/宏替换/去掉注释/条件编译                      (test.i main .i)
    编译,    检查语法,生成汇编                                                      ( test.s  main .s)
    汇编,   汇编代码转换机器码                                                         (test.o main.o)
    链接     链接到一起生成可执行程序                                              a.out
     

    预处理

    预处理如锲子中所言,是一种展开,下表是常用的一些预处理命令

    还有下列几种预处理宏(是双下划线)

    __LINE__ 表示正在编译的文件的行号
    __FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"
    __TIME__ 表示编译时刻的时间字符串,例如: "12:30:55"
    __STDC__ 判断该文件是不是定义成标准 C 程序
    我的vs2013不是定义的标准c语言

     

    宏函数很好用,是直接展开,在这我顺便说一下宏的好处和坏处。

    宏优点1代码复用性2提高性能

    宏缺点1 不可调试(预编译阶段进行了替换),2无类型安全检查3可读性差,容易出错。

    这里附上《c和指针》中的一张表格,总结宏和函数十分到位,我就不多说了

     

    宏函数很皮,#define定义一个比如判断大小,替换常量,很是方便。

    不过我现在也就用下,#define ERROR_POWEROFF -1,#define _CRT_SECURE_NO_WARNINGS 1这样的和编译器有关的东西,不会去写宏函数,宏函数这东西,可读性特别差,在c++中,一般用const/枚举/内联去替代宏。

    但是,define宏在某些方面真的是非常好用,我很推荐。

    1.替代路径

    #define ENG_PATH_1 C:\Program Files (x86)

    2.针对编译器版本不兼容报错

    #define _CRT_SECURE_NO_WARNINGS 1

    3.条件编译

    #ifdef 标识符
    程序段 1
    #else
    程序段 2
    #endif

    4.使用库中的宏

    vc++中有许多有意思的宏,都是大牛们写出来的,真的是充满智慧,十分刁钻,怎么学也学不完,我个人担心出错就很少写宏,用函数代替了。在以后的博客中我会记录一些常用的,充作笔记。

    emmm,当然,还有其他许多重要的预处理。

    比如

    include

    #include <filename>

    尖括号是预处理到系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头文件)。找到文件后,用文件内容替换该语句。如stdio.h

    #include“filename”

    “”则是预处理我们自己第三方的文件,如程序员小刘写的Date.h,我们就可以include“Date.h”

    #error 预处理,#line 预处理,#pragma 预处理

    #error 预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。

    这个我没写过,但碰到过很多次,在编写mfc代码中,拉入控件时我加入密码框控件,OS编译时会自动弹出#error 提示我该编辑框为密码,注意明文问题

    #line 的作用是改变当前行数和文件名称,如#line 28  liu 

    目前我没使其派上用场,但了解为好。

    #pragma 是比较重要且困难的预处理指令。

    #pragma once 

    这个的做用就是防止头文件多次包含

    当然,还有另外一种风格,防止被包含,我同时给出来

    是巧妙地利用了define宏

    #ifndef _SOME_H
    #define _SOME_H
    
    
    ...//(some.h头文件内容)
    
    
    #endif

    变量的防止重复定义则利用extern,在头文件中不初始化只声明。引用该头文件即可,在链接过程中。就可以使用到这个变量。

    (附:extern在c++中经常用于  extern "C"  告诉编译器下面是c语言风格)

    #pragma warning

    #pragma warning( disable : 4507 34; once : 4385; error : 164 )
    等价于:
    #pragma warning(disable:4507 34) // 不显示 4507 和 34 号警告信息
    #pragma warning(once:4385) // 4385 号警告信息仅报告一次
    #pragma warning(error:164) // 把 164 号警告信息作为一个错误。

    另外还有

    #pragma pack

    使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
    使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
    在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。

    字节对齐,我将另起炉灶,在另外一篇博客中归纳总结。

     

    #pragma pack(push) //保存当前对其方式到 packing stack
    #pragma pack(push,n) 等效于
    #pragma pack(push)
    #pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按 n 字节对齐
    #pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐

    #运算符和##预算符

    #define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));

    这段代码中#就是帮助x作为一个变量,表现出来,而不是一个简单的字母

    如果有#,SQR(3)运算出来就是

    The square of 3  is 9

    如果没有# SQL(3)运算出来就是

    The square of x  is 9

    ##预算符

    ##把两个语言符号组合成单个语言符号

    编译

    编译阶段是检查语法,生成汇编,这个属于程序员的必备知识,我们学习一门语言第一步就是知晓语法,其中比较生涩的有左值右值,指针的使用,内存的管理,数据结构的使用,这将会是一场持久战 ,贯穿在整个学习生涯。

    在这里我截取优先级问题,这个可能会通过编译但是不一定达到程序员想要的结果。

    在这里,我引用《c语言深度解剖》中的一张表格

    汇编

      汇编代码转换机器码   这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。

    链接

    开头我引用一下百度百科的介绍

    静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

    动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

    将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件会变大很多,一般是调用自己电脑上的。

    静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,文件生效时才会调用。

    很多代码编译通过,链接失败就极有可能在静态库和动态库这出现了纰漏,要视情况解决。缺少相关所需文件,就会链接报错。这个时候就要检查下本地的链接库是不是缺损。

    展开全文
  • C/C++程序编译过程详解

    万次阅读 多人点赞 2017-11-13 14:51:04
    C/C++程序编译过程详解 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件...

    C/C++程序编译过程详解

    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织,形成最终生成可执行代码的过程。过程图解如下:

    clip_image002

    从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。

    1. 编译过程

    编译过程又可以分成两个阶段:编译和汇编。

    编译

    编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段:

    编译预处理

    读取c源程序,对其中的伪指令(以# 开头的指令)和特殊符号进行处理。

    伪指令主要包括以下四个方面:

    1) 宏定义指令,如# define Name TokenString,# undef等。

    对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。

    2) 条件编译指令,如# ifdef,# ifndef,# else,# elif,# endif等。

    这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

    3) 头文件包含指令,如# include "FileName" 或者# include < FileName> 等。

    在头文件中一般用伪指令# define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。

    采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条# include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

    包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/ usr/ include目录下。在程序中# include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在# include中要用双引号("")。

    4) 特殊符号,预编译程序可以识别一些特殊的符号。

    例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

    预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。

    编译、优化阶段

    经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。

    编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

    优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

    对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

    后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

    经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

    汇编

    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

    目标文件由段组成。通常一个目标文件中至少有两个段:

    1) 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

    2) 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    UNIX环境下主要有三种类型的目标文件:

    1) 可重定位文件

    其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

    2) 共享的目标文件

    这种文件存放了适合于在两种上下文里链接的代码和数据。

    第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;

    第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

    3) 可执行文件

    它包含了一个可以被操作系统创建一个进程来执行之的文件。

    汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

    2. 链接过程

    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

    例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

    根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

    1) 静态链接

    在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

    2) 动态链接

    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

    对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

    3. GCC的编译链接

    我们在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,这的确方便了编译工作,但对于初学者了解编译过程就很不利了,下图便是gcc代理的编译过程:

    clip_image004

    从上图可以看到:

    1) 预编译

    将.c 文件转化成 .i文件

    使用的gcc命令是:gcc –E

    对应于预处理命令cpp

    2) 编译

    将.c/.h文件转换成.s文件

    使用的gcc命令是:gcc –S

    对应于编译命令 cc –S

    3) 汇编

    将.s 文件转化成 .o文件

    使用的gcc 命令是:gcc –c

    对应于汇编命令是 as

    4) 链接

    将.o文件转化成可执行程序

    使用的gcc 命令是: gcc

    对应于链接命令是 ld

    总结起来编译过程就上面的四个过程:预编译处理(.c) --> 编译、优化程序(.s、.asm)--> 汇编程序(.obj、.o、.a、.ko) --> 链接程序(.exe、.elf、.axf等)。

    4. 总结

    C语言编译的整个过程是非常复杂的,里面涉及到的编译器知识、硬件知识、工具链知识都是非常多的,深入了解整个编译过程对工程师理解应用程序的编写是有很大帮助的,希望大家可以多了解一些,在遇到问题时多思考、多实践。

    一般情况下,我们只需要知道分成编译和链接两个阶段,编译阶段将源程序(*.c) 转换成为目标代码(一般是obj文件,至于具体过程就是上面说的那些阶段),链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)就可以了,其他的都需要在实践中多多体会才能有更深的理解。

     


    C/C++编译过程

    C/C++编译过程主要分为4个过程 
    1) 编译预处理 
    2) 编译、优化阶段 
    3) 汇编过程 
    4) 链接程序

    一、编译预处理

    (1)宏定义指令,如#define Name TokenString,#undef等。 对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,

    但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。

    (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。

    预编译程序将根据有关的文件,将那些不必要的代码过滤掉

    (3) 头文件包含指令,如#include "FileName"或者#include <FileName>等。 在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),

    同时包含有各种外部符号的声明。 包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。

    在程序中#include它们要使用尖括号(< >)。

    另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。

    (4)特殊符号,预编译程序可以识别一些特殊的符号。 例如在源程序中出现的#line标识将被解释为当前行号(十进制数), 
    上面程序实现了对宏line的运用

    (5)预处理模块 预处理工作由#pragma命令完成,#Pragma命令将设定编译器的状态或者是指示编译器完成一些特定的动作。

    #pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。

    依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。 
    打开C标准库函数,如stdio.h,我们总能找到下面这一句指示编译器初始化堆栈

    复制代码
    #include "iostream"
    #line 100
    using namespace std;
    int main(int argc, char* argv[])
    {
    cout<<"__LINE__:"<<__LINE__<<endl;
    return 0;
    }
    复制代码

    /*-------------------- 
    * 输出结果为: 
    * __LINE__:103 
    * 本来输出的结果应该是 7,但是用#line指定行号之后,使下一行的行号变为, 
    * 到输出语句恰为行103 
    ---------------------*/ 
    C/C++编译过程 
    或者程序指示编译器去链接系统动态链接库或用户自定义链接库 
    二、编译、优化阶段 
    经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。 
    在《编译原理》中我们可以了解到一个编译器对程序代码的编译主要分为下面几个过程: 
    a) 词法分析 
    b) 语法分析 
    c) 语义分析 
    d) 中间代码生成 
    e) 代码优化 
    f) 代码生成 
    g) 符号表管理 
    h) 将多个步骤组合成趟 
    i) 编译器构造工具
     
    在这里我们主要强调对函数压栈方式(函数调用约定)的编译处理 
    C与C++语言调用方式大体相同,下面是几种常用的调用方式:

    __cdecl 是C DECLaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,

    这些参数由调用者清除,称为手动清栈。被调用函数不需要求调用者传递多少参数,调用者传递过多或者过少的参数,

    甚至完全不同的参数都不会产生编译阶段的错误。

    _stdcall 是StandardCall的缩写,是C++的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话,

    最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retnX,X表示参数占用的字节数,

    CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。函数在编译的时候就必须确定参数个数,

    并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。

    PASCAL 是Pascal语言的函数调用方式,在早期的c/c++语言中使用这种调用方式,

    参数压栈顺序与前两者相反,但现在我们在程序中见到的都是它的演化版本,其实 

    复制代码
    #pragma comment(lib,_T("GDI32.lib"))
    #ifdef _MSC_VER
    /*
    * Currently, all MS C compilers for Win32 platforms default to 8 byte
    * alignment.
    */
    #pragma pack(push,_CRT_PACKING)
    #endif /* _MSC_VER */
    复制代码

    C/C++编译过程 
    质是另一种调用方式 
    _fastcall是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此_fastcall通常规定将前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同。返回方式和_stdcall相当。 
    _thiscall 是为了解决类成员调用中this指针传递而规定的。_thiscall要求把this指针放在特定寄存器中,该寄存器由编译器决定。VC使用ecx,Borland的C++编译器使用eax。返回方式和_stdcall相当。 
    _fastcall 和 _thiscall涉及的寄存器由编译器决定,因此不能用作跨编译器的接口。所以Windows上的COM对象接口都定义为_stdcall调用方式。 
    C中不加说明默认函数为_cdecl方式(C中也只能用这种方式),C++也一样,但是默认的调用方式可以在IDE环境中设置。简单的我们可以从printf函数看出 
    printf使用从从左至右压栈,返回int型并由_CRTIMP指定封在动态链接库中。 
    通过金典的hello world程序我们可以知道编译器对其argc和argv[]这两个参数进行了压栈,并且argc留在了栈顶 
    优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化处理主要分为下面几个过程: 
    1) 局部优化 
    a) 基本块的划分 
    b) 基本块的变换 
    c) 基本块的DAG表示 
    d) DAG的应用 
    e) 构造算法讨论 
    2) 控制流分析和循环优化 
    a) 程序流图与循环 
    复制代码
    /*金典的hello world*/
    #include <stdio.h>
    int main(int argc, char* argv[])
    {
    printf("hello world");
    return 0;
    }
    _Check_return_opt_ _CRTIMP int __cdecl printf(_In_z_ _Printf_format_string_ const char * _Format, ...);
    #define CALLBACK _stdcall /* Windows程序回调函数*/
    #define WINAPI _stdcall
    #define WINAPIV _cdecl
    #define PASCAL _stdcall /*在c++语言中使用了StandardCall调用方式*/
    #define PASCAL _cdecl/*在c语言中使用了C DECLaration调用方式*/
    复制代码

    C/C++编译过程 
    b) 循环 
    c) 循环的查找 
    d) 可归约流图 
    e) 循环优化 
    3) 数据流的分析与全局优化 
    a) 一些主要的概念 
    b) 数据流方程的一般形式 
    c) 到达一定值数据流方程 
    d) 可用表达式及其数据流方程 
    e) 活跃变量数据流方程 
    f) 复写传播
     
    经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

    三、汇编过程

    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,

    都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

    目标文件由段组成。通常一个目标文件中至少有两个段: 代码段:该段中所包含的主要是程序的指令。

    该段一般是可读和可执行的,但一般却不可写。 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    四、链接程序

    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

    例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);

    在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,

    使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

    根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

    (1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。

    这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,

    其中的每个文件含有库中的一个或者一组相关函数的代码。

    (2) 动态链接 
    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。C/C++编译过程对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

    ----------------------------------------------------作者  张彦升

    转自:https://www.cnblogs.com/mickole/articles/3659112.html

    展开全文
  • Android编译系统简要介绍和学习计划

    万次阅读 多人点赞 2017-01-06 12:45:53
    在Android源码环境中,我们开发好一个模块后,再写一个Android.mk文件,就可通过m/mm/mmm/make等命令进行编译。此外,通过make命令还可制作各种系统镜像文件,例如system.img、boot.img和recovery.img等。这一切都...

            在Android源码环境中,我们开发好一个模块后,再写一个Android.mk文件,就可通过m/mm/mmm/make等命令进行编译。此外,通过make命令还可制作各种系统镜像文件,例如system.img、boot.img和recovery.img等。这一切都得益于Android编译系统,它为我们处理了各种依赖关系,以及提供各种有用工具。本文对Android编译系统进行简单介绍以及制定学习计划。

    老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

    《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

            在研究Android编译系统之前,我们首先需要了解Linux系统的make命令。在Linux系统中,我们可以通过make命令来编译代码。Make命令在执行的时候,默认会在当前目录找到一个Makefile文件,然后根据Makefile文件中的指令来对代码进行编译。也就是说,make命令执行的是Makefile文件中的指令。Makefile文件中的指令可以是编译命令,例如gcc,也可以是其它命令,例如Linux系统中的shell命令cp、rm等等。理解这一点非常重要,因为虽然通常我们说make命令是可以编译代码的,但是它实际上可以做任何事情。

            看到这里,有的小伙伴可能会说,在Linux系统中,直接通过shell命令也可以做很多事情啊,它和make命令有什么区别呢?通过前面的介绍可以知道,make命令事实也是通过shell命令来完成任务的,但是它的神奇之处是可以帮我们处理好文件之间的依赖关系。我们通常都有会这样的一个需求,假设有一个文件T,它依赖于另外一个文件D,要求只有当文件D的内容发生变化,才重新生成文件T。这种需求在编译系统中表现得尤其典型,当一个*.c文件include的*.h文件发生变化时,需要重新编译该*.c文件,或者当一个模块A所引用的模块B发生变化时,重新编译模块B。正是由于编译系统中存在这种典型的文件依赖需求,而make命令又是专门用来解决这种文件依赖问题的,因此我们通常认为make命令是用来编译代码的。

            Make命令是怎么知道两个文件之间存在依赖关系,以及当被依赖文件发生变化时如何处理目标文件的呢?答案就在前面提到的Makefile文件。Makefile文件实际上是一个脚本文件,就像普通的shell脚本文件一样,只不过它遵循的是Makefile语法。Makefile文件最基础的功能就是描述文件之间的依赖关系,以及怎么处理这些依赖关系。例如,假设有一个目录文件target,它依赖于文件dependency,并且当文件dependency发生变化时,需要通过command命令来重新生成文件T,这时候我们就可以在Makefile编写以下语句:

    target: dependency
    <tab>command -o target -i dependency
            我们假设命令command的-o选项指定的是输出文件,而-i选项指定的是输入文件。此外,命令command必须是另起一行,并且以tab键开头。

            这就是最基础也是最主要的Makefile文件语法。当然,Makefile文件还有很多其它的语法,这里不可能一一描述。老罗推荐一本书《GNU make中文手册》,里面非常详细地介绍了make以及Makefile文件语法。

            通常我们在一个源代码工程中,包含有非常多模块,模块之间保持相对独立。我们说相对独立,是指模块之间只在接口上存在依赖,但是在实现上是相全独立的。例如,我们有一个SO模块A,它引用了另一个SO模块B的一个导出函数F。只要函数F的原型不变,那么模块B就可以独立于模块A来实现,而模块A只需要一个包含有函数F原型声明的头文件即可对模块B进行引用。在一个源代码工程中,使得模块之间保持上述的相对独立非常重要,否则的话,当模块多了之后,就会很容易造成混乱。

            当一个源代码工程被划分成很多个相对独立的模块之后,我们就很直觉地想到,为每一个模块都编译写一个Makefile文件,使得它们可以独立编译,然后再在工程的根目录下编写一个Makefile文件来递归执行这些Makefile文件。事实上,这种做法是错误的。正确的做法无论工程有多少个模块,都应该只有一个Makefile文件。注意,即使整个工程只有一个Makefile文件,但是我们仍然是要求对工程进行模块划分,并且需要保持这些模块之间的相对独立性,否则的话,就会同样引发混乱。在整个工程只有一个Makefile文件的情况下,我们要求工程的各个模块拥有自己的Makefile片段。这些Makefile片段统统直接或者间接地通过Makefile语法中的include指令包含在工程的Makefile文件中,然后再进行编译。

            为什么我们要求整个工程只有一个Makefile文件,而不是工程的每一个模块都拥有一个Makefile文件呢?直觉告诉我们,每一个模块都拥有一个Makefile文件就会显得更加独立。事实的确如此,每一个模块都拥有自己的Makefile文件可以使得它更加独立。但是这种方法在处理模块之间的依赖关系时会显得力不从心,导致make命令要么是做得太少(do too little),要么是做得太多(do too much)。Make命令做得太少意味着编译结果不正确,而做得太多意味着编译速度慢。注意,出现这种情况并不是make命令本身的设计有问题,而是因为我们给了它不好的输入,就是所谓的"垃圾进,垃圾出"(Garbage In, Garbage Out)。

            早在1998年的时候,Peter Miller就发现了为工程的每一个模块都编写一个Makefile文件的种种坏处,并且发表有一篇论文Recursive Make Considered Harmful。有兴趣的小伙伴可以读一下,写得非常好。这里我们简单从这篇论文摘录一下为什么Recursive Make会导致“do too little”和“do too much”。

            假设我们有一个工程,它的目录结构如下所示:

    Project
      ----Makefile
      ----ant
           ----Makefile
           ----main.c
      ----bee
           ----Makefile
           ----parse.c
           ----parse.h
          顶层目录的Makefile的内容如下所示:

    MODULES = ant bee
    all:
        for dir in $(MODULES); do \
            (cd $$dir; $(MAKE) all); \
        done
          ant目录下的Makefile的内容如下所示:

    all: main.o
    main.o: main.c ../bee/parse.h
        $(CC) -I../bee -c main.c
           通过图1的无回路有向图(DAG)可以清楚看到ant目录的Makefile所描述的文件依赖关系:

    图1 ant模块的文件依赖关系图

     
           bee目录下的Makefile的内容如下所示:

    OBJ = ../ant/main.o parse.o
    all: prog
    prog: $(OBJ)
        $(CC) -o $@ $(OBJ)
    parse.o: parser.c parse.h
        $(CC) -c parse.c
           通过图2的无回路有向图(DAG)可以清楚看到bee目录的Makefile所描述的文件依赖关系:

    图2 bee模块的文件依赖关系图

            
           到目前为止,一切都正常,我们在工程根目录下执行make命令,就可以递归对ant和bee模块进行编译,并且最终生成可执行文件prog。

           考虑一个情景,bee目录的parse.h和parse.c文件是通过yacc工具自动生成的,也就是我们在bee/Makefile文件增加以下内容来生成parse.h和parse.c文件:

    parse.c parse.h: parse.y
        $(YACC) -d parse.y
        mv y.tab.c parse.c
        mv y.tab.h parse.h
           这时候bee模块的文件依赖关系图如图3所示:

    图3 修改后的bee模块文件依赖关系图

           这时候如果我们修改了parse.y文件,那么在工程根目录执行make命令时,先会对ant模块进行编译,然后再对bee模块进行编译。由于对ant模块进行编译时,parse.h文件的内容还没有被修改,因此该文件就认为是最新的,于是就不会重新生成main.o文件。接下来对bee模块进行编译时,就会重新生成parse.h和parse.c文件,并且重新生成parse.o文件。很遗憾,在重新生成prog文件时,使用的是过旧的main.o文件,于是就会得到不正确的prog文件。

           产生这个问题的原因就在于ant模块缺乏对parse.h文件的掌控,也就是不知道parse.h文件是如何产生的。如果我们坚持使用这种递归Makefile的方法来编译工程,并且希望解决上述问题,那么有三种方法:

           1. 修改工程目录的Makefile,使得它先对bee模块进行编译,再对ant模块进行编译。然而,这就相当于是要求我们需要明确地指定工程的每一个模块的编译顺序。在一个包含有非常多模块的工程里面,要确定每一个模块的编译顺序是相当困难的。而且当我们新增、修改或者删除模块之后,又需要重新确定各个模块的编译顺序。理想的做法是让make自动地帮我们决定各个模块的编译顺序,这也是我们使用make命令来编译工程的初衷。

           2. 对工程的各个模块进行重复编译,也就是将工程目录的Makefile修改为以下的内容:

    MODULES = ant bee
    all:
        for dir in $(MODULES); do \
            (cd $$dir; $(MAKE) all); \
        done 
        for dir in $(MODULES); do \
            (cd $$dir; $(MAKE) all); \
        done
           这里我们只对每一个模块进行了一次重复编译。然而,在一个复杂的工程中,有可能需要多次进行重复编译才能得到正确的编译结果。与前一种方法(do too little)相比,这里明显就是“do too much”。这会使得我们浪费CPU资源,并且拖慢整个工程的编译速度。

           3. 修改ant/Makefile文件,增加如下所示的内容:

    .PHONY: ../bee/parse.h
    ../bee/parse.h:
        cd ../bee; \
        make clean; \
        make all
            这种方法在每次编译ant模块之前,都先对bee模块进行重新编译,无论它是否需要。与第2种方法相比,这种方法不用对工程的每一个模块都进行重新编译,但是它仍然是“do too much”,因为不管bee模块是否需要重新编译,它都会被重新编译。

            由此可见,上述三种方法,要么是“do too little”,要么是“do too much”,它们虽然都能解决问题,但都不是理想的解决方案。如果我们进一步分析问题的根源,就会发现工程的文件依赖关系是属于工程级别的,也就是它们属于一个整体,而当我们将它们划分成模块级别的时候,就会造成各个模块只看到一部分的文件依赖关系,因此就不能得到正确的编译结果。

            为了保持工程文件依赖关系的整体性,我们就必须使得整个工程只存在一个Makefile。当整个工程只存在一个Makefile时,我们就可以很容易地构造出如图4所示的文件依赖图:


    图4 完整的文件依赖关系图

            整个工程只有一个Makefile,听起来似乎是一件很疯狂的事情,因为这个Makefile可能会变得无比庞大和复杂。其实不用担心,我们可以按照模块来将这个Makefile划分成一个个Makefile片段(fragement),然后通过Makefile的include指令来将这些Makefile片段组装在一个Makefile中。与递归Makefile相比,每一个模块现在拥有的是一个Makefile片段,而不是一个Makefile文件。这正是Android编译系统的设计思想和原则,也就是说,我们平时所编写的Android.mk编译脚本都只不过是整个Android编译系统的一个Makefile片段。

            明白了Android编译系统的设计思想和原则之后,我们就可以通过图5来观察一下Android编译系统的整体架构了:


    图5 Android编译系统架构

           在使用Android编译系统之前,我们需要打开一个shell进入到Android源码根目录中,并且在该shell中将build/envsetup.sh脚本文件source进来。脚本文件build/envsetup.sh被source到当前shell的过程中,会在vendor和device两个目录将厂商指定的envsetup.sh也source到当前shell当中,这样就可以获得厂商提供的产品配置信息。此外,脚本文件build/envsetup.sh还提供了以下几个重要的命令来帮助我们编译Android源码:

           1. lunch

            用来初始化编译环境,例如设置环境变量和指定目标产品型号。Lunch命令在执行的时候,主要做两件事情。第一件事情是设置TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_BUILD_TYPE和TARGET_BUILD_APPS等环境变量,用来指定目标产品类型和编译类型。第二件事情是通过make命令执行build/core/config.mk脚本,并且通过加载另外一个脚本build/core/dumpvar.mk打印出当前的编译环境配置信息。注意,build/core/config.mk和build/core/dumpvar.mk均为Makefile脚本,因此它们可以通过make命令来执行。另外,build/core/config.mk脚本还会加载一个名称为BoradConfig.mk的脚本以及build/core/envsetup.mk脚本来配置目标产品型号的相关信息。

           2. m

           相当于是在执行make命令。对整个Android源码进行编译。

           3. mm

           如果是在Android源码根目录下执行,那么就相当于是执行make命令对整个源码进行编译。如果是在Android源码根目录下的某一个子目录执行,那么就在会在从该子目录开始,一直往上一个目录直至到根目录,寻找是否存在一个Android.mk文件。如果存在的话,那么就通过make命令对该Android.mk文件描述的模块进行编译。

           4. mmm

           后面可以跟一个或者若干个目录。如果指定了多个目录,那么目录之间以空格分隔,并且每一个目录下都必须存在一个Android,mk文件。如果没有在目录后面通过冒号指定模块名称,那么在Android.mk文件中描述的所有模块都会被编译,否则只有指定的模块会被编译。如果需要同时指定多个模块,那么这些模块名称必须以逗号分隔。它的语法如下所示:

    mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]
           该命令会通过make命令来执行Android源码根目录下的Makefile文件,该Makefile文件又会将build/core/main.mk加载进来。文件build/core/main.mk在加载的过程中,还会加载以下几个主要的文件:

           (1). build/core/config.mk

           该文件根据lunch命令所配置的产品信息在build/target/board、vendor或者device目录中找到对应的BoradConfig.mk文件,以及通过加载build/core/product_config.mk文件在build/target/product、vendor或者device目录中找到对应的AndroidProducts.mk文件,来进一步对编译环境进行配置,以便接下来编译指定模块时可以获得必要的信息。

           (2). build/core/definitions.mk

           该文件定义了在编译过程需要调用到的各种自定义函数。

           (3). 指定的Android.mk

           这些指定的Android.mk环境是由mmm命令通过环境变量ONE_SHOT_MAKEFILE传递给build/core/main.mk文件使用的。这些Android.mk文件一般还会通过环境变量BUILD_PACKAGE、BUILD_JAVA_LIBRARY、BUILD_STATIC_JAVA_LIBRARY、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY、BUILD_EXECUTABLE和BUILD_PREBUILT将build/core/package.mk、build/core/java_library.mk、build/core/static_java_library.mk、build/core/shared_library.mk、build/core/static_library.mk、build/core/executable.mk和build/core/prebuilt.mk等编译片段模板文件加载进来,来表示要编译是APK、Java库、Linux动态库/静态库/可执行文件或者预先编译好的文件等等。

           (4). build/core/Makefile

           该文件包含了用来制作system.img、ramdisk.img、boot.img和recovery.img等镜像文件的脚本。

           在接下来的一系列文章中,我们将按照以下情景来进一步分析Android的编译系统:

           1. Android编译系统的环境初始化过程

           2. 模块编译命令mmm的执行过程

           3. Android镜像文件的制作过程

           希望通过这三个情景的分析,使得我们对Android的编译系统有一个深刻的认识,以提高我们的Android系统开发效率,敬请关注!更多信息可以关注老罗的新浪微博:http://weibo.com/shengyangluo

    展开全文
  • 编译原理:总结

    万次阅读 多人点赞 2020-10-12 13:23:51
    编译器概述 编译器的核心功能 编译器的核心功能是把源代码翻译成目标代码: 翻译!!!目标代码!!! 理解源代码:词法分析、语法...转化为等价的目标代码:中间代码生成、目标代码生成 ...语法分析器:单词流-&am
  • 程序的编译与执行过程

    千次阅读 2018-02-25 23:36:43
    本文以C程序为例。 构建C程序需要4个步骤,分别使用4个工具完成: preprocessor, ...第二步,编译. 将第一步产生的文件连同其他源文件一起编译成汇编代码。 第三步,汇编。将第二步产生的汇编源码转换为 object fi...
  • 深入理解程序从编译到运行

    千次阅读 多人点赞 2019-02-17 22:03:56
    From:http://blog.chinaunix.net/uid-22327815-id-3540305.html 从Hello World说程序运行机制:http://www.sohu.com/a/132798003_505868 C/C++中如何在main()函数之前执行一条语句?...(深入理解计算机系统...
  • 编译和链接的过程

    万次阅读 多人点赞 2018-07-22 23:08:24
    程序要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。接下来通过几个简单的例子来详细讲解一下这些过程。 对于上边用到的几个选项需要说明一下。 使用 gcc 命令不跟任何的选项的话,会默认执行...
  • 编译原理学习(一)--编译以及编译过程

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

    千次阅读 多人点赞 2019-07-09 17:15:44
    一、编译和解释 编译:将源代码一次性转换成目标代码的过程 类似英语中的全文翻译。 执行编译过程的程序叫做编译器。 解释:将源代码逐条转换成目标代码同时逐条运行的过程。 类似英语中的同声传译。 ...
  • 简单的编译流程

    千次阅读 2018-10-30 23:35:44
    简易编译器流程图: 一个典型的编译器,可以包含为一个前端,一个后端。前端接收源程序产生一个中间表示,后端接收中间表示继续生成一个目标程序。所以,前端处理的是跟源语言有关的属性,后端处理跟目标机器有关...
  • Android反编译工具包(升级)官方绿色版

    万次下载 热门讨论 2020-07-30 23:30:40
    Android反编译工具包,内含图形和命令两种反编译方式,命令支持windows和linux平台,亲测验证成功!详见博客:Android APK反编译详解(附图) http://blog.csdn.net/sunboy_2050/article/details/6727581
  • apktool 反编译工具 绿色版

    万次下载 热门讨论 2020-07-30 09:07:32
    apktool功能:反编译出apk资源文件。 使用方式: 把apktool 解压到任意位置 执行 在dos 改目录下 执行 apktool d xxx.apk test ,便会把编译后的资源存入test文件夹下。
  • 2020年支持java8的Java反编译工具汇总

    万次阅读 多人点赞 2020-04-30 15:54:14
    luyten是一款操作简单、功能实用的java反编译工具,软件支持*.JAR、*.zip、*.class等类型文件的反编译操作,还原度非常高,支持更多功能设置,如显式导入、类型、合成组件等等,用户可根据不同的需求选择合适的显示...
  • Android APK反编译详解(附图)

    万次阅读 多人点赞 2014-01-13 13:44:08
    这段时间在学Android应用开发,在想既然是用Java开发的应该很好反编译从而得到源代码吧,google了一下,确实很简单,以下是我的实践过程。在此郑重声明,贴出来的目的不是为了去破解人家的软件,完全是一种学习的...
  • 项目使用gulp来处理日常构建,但是当样式文件越来越多后如果每次都是全量编译会导致效率及其差,那么就来解决一下吧~ 问题: gulp-sass每次都会全量编译路径下所有*.scss 解决方案: 每次只将变动的文件进行编译,...
  • APK反编译

    万次阅读 多人点赞 2018-05-17 14:55:52
    学习和开发Android应用有一段时间了,今天写一篇博客总结一下Android的apk文件反编译。我们知道,Android应用开发完成之后,我们最终都会将应用打包成一个apk文件,然后让用户通过手机或者平板电脑下载下来进行安装...
  • 我们都知道,Android程序打完包之后得到的是一个APK文件,这个文件是可以直接安装到任何Android手机上的,我们反编译其实也就是对这个APK文件进行反编译。Android的反编译主要又分为两个部分,一个是对代码的反编译...
  • Android APK反编译就这么简单 详解(附图)

    万次阅读 多人点赞 2014-03-24 20:12:14
    你往往会去借鉴别人的应用是怎么开发的,那些漂亮的动画和精致的布局可能会让你爱不释手,作为一个开发者,你可能会很想知道这些效果界面是怎么去实现的,这时,你便可以对改应用的APK进行反编译查看。下面是我参考...
  • ijkplayer编译so库真没那么难

    万次阅读 2018-01-22 22:53:57
    ijkplayer编译so库真没那么难 引言: 公司现在的电台项目是我第二个接触音频播放项目,Android音视频 播放很多还是使用的MediaPlayer(大中厂除外),但是如果你用过 MediaPlayer的话,很多开发者都会吐槽有多坑...
1 2 3 4 5 ... 20
收藏数 3,300,748
精华内容 1,320,299
关键字:

编译