>  기사  >  시스템 튜토리얼  >  Linux 프로그램 컴파일 과정에 대한 자세한 설명

Linux 프로그램 컴파일 과정에 대한 자세한 설명

WBOY
WBOY앞으로
2024-02-05 13:09:15640검색

컴퓨터 프로그래밍 언어는 일반적으로 기계어, 어셈블리 언어, 고급 언어의 세 가지 범주로 나뉩니다. 고급 언어를 실행하려면 먼저 기계어로 번역해야 합니다. 번역에는 컴파일 방식과 해석 방식이 있습니다.

그래서 우리는 기본적으로 고급 언어를 두 가지 범주로 나눕니다. 하나는 C, C++, Java와 같은 컴파일 언어이고 다른 하나는 Python, Ruby, MATLAB 및 JavaScript와 같은 해석 언어입니다.

이 기사에서는 다음 네 단계를 포함하여 C/C++ 언어로 작성된 고급 프로그램을 프로세서에서 실행할 수 있는 바이너리 코드로 변환하는 프로세스를 소개합니다.

  • 전처리
  • 편집
  • 조립
  • 연결
Linux 程序编译过程详解

GCC 도구 체인 소개

흔히 GCC라고 부르는 것은 GUN Compiler Collection의 약자로 Linux 시스템에서 흔히 사용되는 컴파일 도구입니다. GCC 도구 체인 소프트웨어에는 GCC, Binutils, C 런타임 라이브러리 등이 포함됩니다.

GCC

GCC(GNU C 컴파일러)는 컴파일 도구입니다. 이번 글에서는 C/C++ 언어로 작성된 프로그램을 프로세서에서 실행할 수 있는 바이너리 코드로 변환하고, 컴파일러에서 완성하는 과정을 소개하겠습니다.

Binutils

addr2line, ar, objcopy, objdump, as, ld, ldd, readelf, size 등을 포함한 바이너리 프로그램 처리 도구 세트 이 도구 세트는 개발 및 디버깅에 없어서는 안될 도구입니다. 각각의 소개는 다음과 같습니다.

  • addr2line: 프로그램 주소를 해당 프로그램 소스 파일 및 해당 코드 라인으로 변환하고 해당 기능을 얻는 데 사용됩니다. 이 도구는 디버거가 디버깅 중에 해당 소스 코드 위치를 찾는 데 도움이 됩니다.
  • as : 주로 조립에 사용됩니다. 조립에 대한 자세한 소개는 다음 기사를 참조하십시오.
  • ld: 주로 링크에 사용됩니다. 링크에 대한 자세한 내용은 다음 문서를 참조하세요.
  • ar: 주로 정적 라이브러리를 만드는 데 사용됩니다. 초보자가 쉽게 이해할 수 있도록 동적 라이브러리와 정적 라이브러리의 개념을 소개합니다.
  • 여러 .o 대상 파일을 하나의 라이브러리 파일로 생성하려는 경우 두 가지 유형의 라이브러리가 있습니다. 하나는 정적 라이브러리이고 다른 하나는 동적 라이브러리입니다.
  • Windows에서 정적 라이브러리는 .lib 접미사가 붙은 파일이고, 공유 라이브러리는 .dll 접미사가 붙는 파일입니다. Linux에서 정적 라이브러리는 접미사가 .a인 파일이고, 공유 라이브러리는 접미사가 .so인 파일입니다.
  • 정적 라이브러리와 동적 라이브러리의 차이점은 코드가 로드되는 시간에 있습니다. 정적 라이브러리의 코드는 컴파일 과정에서 실행 프로그램에 로드되므로 크기가 더 커집니다. 공유 라이브러리의 코드는 실행 프로그램이 실행될 때 메모리에 로드되고 컴파일 과정에서만 참조되므로 코드 크기가 더 작습니다. Linux 시스템에서는 ldd 명령을 사용하여 실행 프로그램이 의존하는 공유 라이브러리를 볼 수 있습니다.
  • 시스템에 동시에 실행해야 하는 여러 프로그램이 있고 이러한 프로그램 간에 공유 라이브러리가 있는 경우 동적 라이브러리를 사용하면 더 많은 메모리가 절약됩니다.
  • ldd: 실행 프로그램이 의존하는 공유 라이브러리를 보는 데 사용할 수 있습니다.
  • objcopy: .bin을 .elf로 변환하거나 .elf를 .bin으로 변환하는 등 개체 파일을 다른 형식으로 변환합니다.
  • objdump: 주요 기능은 분해입니다. 분해에 대한 자세한 소개는 다음 문서를 참조하세요.
  • readelf: ELF 파일에 대한 정보를 표시합니다. 자세한 내용은 나중에 참조하세요.
  • 크기: 코드 세그먼트, 데이터 세그먼트, 전체 크기 등을 포함하여 실행 파일의 각 부분의 크기와 전체 크기를 나열합니다. 크기 사용에 대한 구체적인 사용 예는 다음 문서를 참조하세요.

C 런타임 라이브러리

C 언어 표준은 주로 두 부분으로 구성됩니다. 한 부분은 C의 구문을 설명하고 다른 부분은 C 표준 라이브러리를 설명합니다. C 표준 라이브러리는 표준 헤더 파일 세트를 정의합니다. 각 헤더 파일에는 일부 관련 함수, 변수, 유형 선언 및 매크로 정의가 포함되어 있습니다. 예를 들어, 공통 printf 함수는 C 표준 라이브러리 함수이고 해당 프로토타입은 stdio에 정의되어 있습니다. 헤더 파일.

C语言标准仅仅定义了C标准库函数原型,并没有提供实现。因此,C语言编译器通常需要一个C运行时库(C Run Time Libray,CRT)的支持。C运行时库又常简称为C运行库。与C语言类似,C++也定义了自己的标准,同时提供相关支持库,称为C++运行时库。

准备工作

由于GCC工具链主要是在Linux环境中进行使用,因此本文也将以Linux系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个C语言编写的简单Hello程序作为示例,其源代码如下所示:

#include  

//此程序很简单,仅仅打印一个Hello World的字符串。
int main(void)
{
  printf("Hello World! \n");
  return 0;
}

编译过程

1.预处理

预处理的过程主要包括以下过程:

  • 将所有的#define删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等。
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有注释“//”和“/* */”。
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  • 保留所有的#pragma编译器指令,后续编译过程需要使用它们。

使用gcc进行预处理的命令如下:

$ gcc -E hello.c -o hello.i // 将源文件hello.c文件预处理生成hello.i                      
// GCC的选项-E使GCC在进行完预处理后即停止

hello.i文件可以作为普通文本文件打开进行查看,其代码片段如下所示:

// hello.i代码片段

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int
main(void)
{
  printf("Hello World!" "\n");
  return 0;
}

2.编译

编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。

使用gcc进行编译的命令如下:

$ gcc -S hello.i -o hello.s // 将预处理生成的hello.i文件编译生成汇编程序hello.s
                        // GCC的选项-S使GCC在执行完编译后停止,生成汇编程序

上述命令生成的汇编程序hello.s的代码片段如下所示,其全部为汇编代码。

// hello.s代码片段

main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

3.汇编

汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用Binutils中的汇编器as根据汇编指令和处理器指令的对照表一一翻译即可。

当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。

使用gcc进行汇编的命令如下:

$ gcc -c hello.s -o hello.o // 将编译生成的hello.s文件汇编生成目标文件hello.o
                        // GCC的选项-c使GCC在执行完汇编后停止,生成目标文件
//或者直接调用as进行汇编
$ as -c hello.s -o hello.o //使用Binutils中的as将hello.s文件汇编生成目标文件

注意:hello.o目标文件为ELF(Executable and Linkable Format)格式的可重定向文件。

4.链接

链接也分为静态链接和动态链接,其要点如下:

  • 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
  • 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
  • 在Linux系统中,gcc编译链接时的动态库搜索路径的顺序通常为:首先从gcc命令的参数-L指定的路径寻找;再从环境变量LIBRARY_PATH指定的路径寻址;再从默认路径/lib、/usr/lib、/usr/local/lib寻找。
  • 在Linux系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量LD_LIBRARY_PATH指定的路径寻址;再从配置文件/etc/ld.so.conf中指定的动态库搜索路径;再从默认路径/lib、/usr/lib寻找。
  • 在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。

由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc链接时默认优先选择动态库,会链接libtest.so,如果要让gcc选择链接libtest.a则可以指定gcc选项-static,该选项会强制使用静态库进行链接。以Hello World为例:

  • 如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:
$ gcc hello.c -o hello
$ size hello  //使用size查看大小
   text    data     bss     dec     hex filename
   1183     552       8    1743     6cf     hello
$ ldd hello //可以看出该可执行文件链接了很多其他动态库,主要是Linux的glibc动态库
        linux-vdso.so.1 =>  (0x00007fffefd7c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadcdd82000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fadce14c000)

如果使用命令“gcc -static hello.c -o hello”则会使用静态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:

$ gcc -static hello.c -o hello
$ size hello //使用size查看大小
     text    data     bss     dec     hex filename
 823726    7284    6360  837370   cc6fa     hello //可以看出text的代码尺寸变得极大
$ ldd hello
       not a dynamic executable //说明没有链接动态库

链接器链接后生成的最终文件为ELF格式可执行文件,一个ELF可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss等段。

分析ELF文件

1.ELF文件的段

ELF文件格式如下图所示,位于ELF Header和Section Header Table之间的都是段(Section)。一个典型的ELF文件包含下面几个段:

  • .text:已编译程序的指令代码段。
  • .rodata:ro代表read only,即只读数据(譬如常数const)。
  • .data:已初始化的C程序全局变量和静态局部变量。
  • .bss:未初始化的C程序全局变量和静态局部变量。
  • .debug:调试符号表,调试器用此段的信息帮助调试。
Linux 程序编译过程详解

可以使用readelf -S查看其各个section的信息如下

$ readelf -S hello
There are 31 section headers, starting at offset 0x19d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
……
  [11] .init             PROGBITS         00000000004003c8  000003c8
       000000000000001a  0000000000000000  AX       0     0     4
……
  [14] .text             PROGBITS         0000000000400430  00000430
       0000000000000182  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         00000000004005b4  000005b4
……

2.反汇编ELF

由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。

使用objdump -D对其进行反汇编如下:

$ objdump -D hello
……
0000000000400526 :  // main标签的PC地址
//PC地址:指令编码                  指令的汇编格式
  400526:    55                          push   %rbp 
  400527:    48 89 e5                mov    %rsp,%rbp
  40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
  40052f:    e8 cc fe ff ff          callq  400400 
  400534:    b8 00 00 00 00          mov    $0x0,%eax
  400539:    5d                      pop    %rbp
  40053a:    c3                          retq   
  40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

使用objdump -S将其反汇编并且将其C语言源代码混合显示出来:

$ gcc -o hello -g hello.c //要加上-g选项
$ objdump -S hello
……
0000000000400526 :
#include 

int
main(void)
{
  400526:    55                          push   %rbp
  400527:    48 89 e5                mov    %rsp,%rbp
  printf("Hello World!" "\n");
  40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
  40052f:    e8 cc fe ff ff          callq  400400 
  return 0;
  400534:    b8 00 00 00 00          mov    $0x0,%eax
}
  400539:    5d                          pop    %rbp
  40053a:    c3                          retq   
  40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
……

위 내용은 Linux 프로그램 컴파일 과정에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 lxlinux.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제