0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

【GCC編譯優(yōu)化系列】使用GCC如何把C文件編譯成可執(zhí)行文件

嵌入式物聯(lián)網(wǎng)開(kāi)發(fā) ? 來(lái)源:嵌入式物聯(lián)網(wǎng)開(kāi)發(fā) ? 作者:嵌入式物聯(lián)網(wǎng)開(kāi)發(fā) ? 2022-07-11 09:10 ? 次閱讀

1 前言

自參加RTT論壇問(wèn)答有獎(jiǎng)】活動(dòng)以來(lái),回答了300+問(wèn)題,期間我特意去檢索過(guò)【編譯】相關(guān)的問(wèn)題,從下圖可以看得出,編譯問(wèn)題真的是很常見(jiàn)的問(wèn)題類(lèi)型,不管你是新手還是老手,多多少少都遇到過(guò)奇奇怪怪的編譯問(wèn)題。

image-20211203132811396

而我平時(shí)非常喜歡研究跟編譯相關(guān)的問(wèn)題,期間也挑了好一些編譯相關(guān)的問(wèn)題,給出了我的答案,我也會(huì)盡力在解答問(wèn)題的過(guò)程中,把我解決編譯問(wèn)題用到的方法論也一并分享出來(lái),希望能幫助到大家。

但是由于回答單個(gè)編譯問(wèn)題,畢竟篇幅有限,只能就特定的場(chǎng)景下,如何解決問(wèn)題而展開(kāi),而不能系統(tǒng)地介紹一些代碼編譯相關(guān)的基礎(chǔ)知識(shí),所以我才萌生了通過(guò)自己寫(xiě)一些通識(shí)性比較強(qiáng)的技術(shù)文章來(lái)補(bǔ)充這一部分的知識(shí)空白。

本系列的文章,計(jì)劃安排兩篇文章,第一篇結(jié)合gcc編譯器介紹編譯相關(guān)的基礎(chǔ)知識(shí),第二篇結(jié)合實(shí)際的代碼案例分析如何解決各種編譯相關(guān)的問(wèn)題。當(dāng)然如果大家想了解編譯相關(guān)的其他內(nèi)容,也歡迎在評(píng)論席告知。

本文作為分享的第一篇,主要介紹了C代碼是如何被編譯生成二進(jìn)制文件的詳細(xì)步驟,期間用到了gcc編譯器,希望能提升大家對(duì)C代碼編譯的基礎(chǔ)認(rèn)知以及gcc編譯器的使用技巧。

2 C代碼的編譯步驟

C代碼編譯的步驟,需要經(jīng)歷預(yù)編譯、編譯、匯編、鏈接等幾個(gè)關(guān)鍵步驟,最后才能生成二進(jìn)制文件,而這個(gè)二進(jìn)制文件就是能被CPU識(shí)別并正確執(zhí)行指令的唯一憑證。

整個(gè)過(guò)程有預(yù)編譯、編譯器、匯編器、鏈接器在工作,正如這張圖所展示的這樣:

image-20211203132859047

下面簡(jiǎn)要介紹下,各個(gè)步驟的主要工作。

2.1 預(yù)處理(Preprocessing)

預(yù)編譯,主要體現(xiàn)在這個(gè)預(yù)字,它的處理是在編譯的前面。

C語(yǔ)言里,以“#”號(hào)開(kāi)頭的預(yù)處理指令,如文件包含#include、宏定義制定#define、條件編譯#if等。 在源程序中,這些指令都放在函數(shù)體的外面,可以放在源文件(.c文件)中,也可以放在頭文件(.h)中。 預(yù)編譯這一步要做到事情,就是把預(yù)處理的指令進(jìn)行展開(kāi),這里主要介紹上面提到的三類(lèi)預(yù)處理指令。

#include:這個(gè)就是把后面的文件直接拷貝到預(yù)處理指令的位置,當(dāng)然這里也會(huì)處理依賴(lài)include的問(wèn)題,比如A文件 include B文件,而B(niǎo)文件又include了C文件,那么在A里面是看到C文件的內(nèi)容的。還有有個(gè)盲區(qū)就是,include是可以include xxx.c的,這個(gè)在C語(yǔ)言的語(yǔ)法上是沒(méi)有任何問(wèn)題的,大家千萬(wàn)別以為只能C文件 include 頭文件。#define:這個(gè)就是處理宏定義的展開(kāi),注意宏定義是原封不動(dòng)的展開(kāi)、替換,它是不考慮語(yǔ)法規(guī)則的,這一點(diǎn)在寫(xiě)宏定義的時(shí)候尤其需要注意,有的時(shí)候多寫(xiě)一些包括可以減少因展開(kāi)帶來(lái)的不必要麻煩。#if:這個(gè)就是處理?xiàng)l件編譯,類(lèi)似的預(yù)處理指令有好幾個(gè):#ifdef #ifndef #else #elif #endif等,這些預(yù)處理指令后面接一個(gè)條件,常常用于控制部分代碼參不參與編譯,這也就是我們常說(shuō)的代碼裁剪,絕大多數(shù)的支持裁剪的軟件代碼,都是通過(guò)這種#if條件編譯的形式來(lái)實(shí)現(xiàn)的。

2.2 編譯(Compilation)

這一步是C代碼編譯的真正開(kāi)始,主要是把預(yù)處理之后的C代碼,編譯成匯編代碼;即由高級(jí)語(yǔ)言代碼翻譯成低級(jí)語(yǔ)言代碼。 在編譯過(guò)程中,編譯器主要作語(yǔ)法檢查和詞法分析。在確認(rèn)所有指令都符合語(yǔ)法規(guī)則之后,將其翻譯成等價(jià)的匯編代碼。

2.3 匯編(Assemble)

這一步是將上一步生成的匯編代碼,通過(guò)匯編器,將其轉(zhuǎn)成二進(jìn)制目標(biāo)代碼,這個(gè)就是我們常說(shuō)的obj文件。 經(jīng)過(guò)這一步,單個(gè).c文件就編譯完了;換句話(huà)說(shuō),每一個(gè).c文件編譯到obj文件,都要經(jīng)過(guò)預(yù)編譯、編譯、匯編這三步。

2.4 鏈接(Linking)

這一步是通過(guò)鏈接器,將上一步生成的所有二進(jìn)制目標(biāo)文件、啟動(dòng)代碼、依賴(lài)的庫(kù)文件,一并鏈接成一個(gè)可執(zhí)行文件,這個(gè)可執(zhí)行文件可被加載或拷貝到存儲(chǔ)器去執(zhí)行的。

這里需要注意的是,不同的操作系統(tǒng)下這個(gè)可執(zhí)行文件的格式是不同的:

Windows系統(tǒng)是exe后綴名的可執(zhí)行文件; Linux系統(tǒng)下是elf文件(沒(méi)有后綴名的說(shuō)法),也是可執(zhí)行文件; MacOS系統(tǒng)下是Mach-O文件,也是可執(zhí)行文件。

各種類(lèi)型的可執(zhí)行文件的詳細(xì)分析,可參見(jiàn)我轉(zhuǎn)載的一篇博文。

2.5 生成二進(jìn)制文件(Objcopy)

如果是在嵌入式設(shè)備上,使用類(lèi)似RTOS(Real-Time Operating System)的操作系統(tǒng),因內(nèi)存、存儲(chǔ)等資源受限,他們不具備像PC環(huán)境下的Linux這種高級(jí)操作系統(tǒng)那樣可以解析可執(zhí)行文件,然后把二進(jìn)制的指令代碼搬到CPU上去運(yùn)行,所以在這樣的背景下,我們需要在編譯結(jié)束后,就把可執(zhí)行文件轉(zhuǎn)換成二進(jìn)制代碼文件,也就是我們常說(shuō)的.bin文件。

一般來(lái)說(shuō),在嵌入式設(shè)備中,這種.bin文件是直接燒錄在Flash中的,如果存儲(chǔ)bin文件的Flash支持XIP(eXecute In Place,即芯片內(nèi)執(zhí)行)的話(huà),那么指令代碼是可以直接在Flash內(nèi)執(zhí)行,而不需要搬到內(nèi)存中去,這也是最大化地利用嵌入式有限的資源條件。

在生成二進(jìn)制文件這一步中,不同的編譯器及不同的操作系統(tǒng)下,可能使用的方法是不一樣的,在Linux平臺(tái)下使用的是objcopy命令來(lái)完成這一操作,具體的用法下文會(huì)詳細(xì)介紹。

3 gcc如何編譯C代碼

下面以gcc編譯器為例,介紹下在Linux平臺(tái)下,一個(gè)C代碼工程是如何編譯生成最終的bin文件的。

3.1 gcc命令參數(shù)介紹

在介紹如何使用gcc編譯之前,我們需要先了解下gcc的幾個(gè)重要的命令行參數(shù),這種命令行參數(shù)問(wèn)題,如果不懂就讓命令行自己告訴你吧:

gcc/gcc_helloworld$ gcc --help
Usage: gcc [options] file...
Options:
  -pass-exit-codes         Exit with highest error code from a phase.
  --help                   Display this information.
  --target-help            Display target specific command line options.
  --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...].
                           Display specific types of command line options.
  (Use '-v --help' to display command line options of sub-processes).
  --version                Display compiler version information.
  -dumpspecs               Display all of the built in spec strings.
  -dumpversion             Display the version of the compiler.
  -dumpmachine             Display the compiler's target processor.
  -print-search-dirs       Display the directories in the compiler's search path.
  -print-libgcc-file-name  Display the name of the compiler's companion library.
  -print-file-name=   Display the full path to library .
  -print-prog-name=  Display the full path to compiler component .
  -print-multiarch         Display the target's normalized GNU triplet, used as
                           a component in the library path.
  -print-multi-directory   Display the root directory for versions of libgcc.
  -print-multi-lib         Display the mapping between command line options and
                           multiple library search directories.
  -print-multi-os-directory Display the relative path to OS libraries.
  -print-sysroot           Display the target libraries directory.
  -print-sysroot-headers-suffix Display the sysroot suffix used to find headers.
  -Wa,            Pass comma-separated  on to the assembler.
  -Wp,            Pass comma-separated  on to the preprocessor.
  -Wl,            Pass comma-separated  on to the linker.
  -Xassembler         Pass  on to the assembler.
  -Xpreprocessor      Pass  on to the preprocessor.
  -Xlinker            Pass  on to the linker.
  -save-temps              Do not delete intermediate files.
  -save-temps=        Do not delete intermediate files.
  -no-canonical-prefixes   Do not canonicalize paths when building relative
                           prefixes to other gcc components.
  -pipe                    Use pipes rather than intermediate files.
  -time                    Time the execution of each subprocess.
  -specs=            Override built-in specs with the contents of .
  -std=          Assume that the input sources are for .
  --sysroot=    Use  as the root directory for headers
                           and libraries.
  -B            Add  to the compiler's search paths.
  -v                       Display the programs invoked by the compiler.
  -###                     Like -v but options quoted and commands not executed.
  -E                       Preprocess only; do not compile, assemble or link.
  -S                       Compile only; do not assemble or link.
  -c                       Compile and assemble, but do not link.
  -o                 Place the output into .
  -pie                     Create a dynamically linked position independent
                           executable.
  -shared                  Create a shared library.
  -x             Specify the language of the following input files.
                           Permissible languages include: c c++ assembler none
                           'none' means revert to the default behavior of
                           guessing the language based on the file's extension.

Options starting with -g, -f, -m, -O, -W, or --param are automatically
 passed on to the various sub-processes invoked by gcc.  In order to pass
 other options on to these processes the -W options must be used.

For bug reporting instructions, please see:
.

我們重點(diǎn)要關(guān)注-E-S-c-o選項(xiàng),下面的步驟中分別會(huì)使用到這些選項(xiàng),再詳細(xì)介紹下對(duì)應(yīng)的選項(xiàng)。

  -E                       Preprocess only; do not compile, assemble or link.
  -S                       Compile only; do not assemble or link.
  -c                       Compile and assemble, but do not link.
  -o                 Place the output into .

3.2 helloworld工程的示例C代碼

這個(gè)小工程由3個(gè)文件組成,1個(gè).H頭文件,2個(gè).C源文件:

/* sub.h */
#ifndef __SUB_H__
#define __SUB_H__

#define TEST_NUM 1024

extern int sub_func(int a);

#endif /* __SUB_H__ */
/* sub.c */
#include 

#include "sub.h"

int sub_func(int a)
{
    return a + 1;
}
/* main.c */
#include 

#include "sub.h"

#ifdef USED_FUNC
void used_func(void)
{
    printf("This is a used function !\n");
}
#endif

int main(int argc, const char *argv[])
{
    printf("Hello world !\n");
    printf("TEST_NUM = %d\n", TEST_NUM);
    printf("sub_func() = %d\n", sub_func(1));

#ifdef USED_FUNC
    used_func();
#endif

    return 0;
}

代碼邏輯很簡(jiǎn)單,sub模塊定義了一個(gè)函數(shù)sub_func和一個(gè)宏定義的整型數(shù),提供給main函數(shù)調(diào)用;main函數(shù)里面分別打印hello world,獲取宏定義整型數(shù)的值,調(diào)用sub_func接口,以及根據(jù)USED_FUNC是否被定義再?zèng)Q定是否調(diào)用used_func函數(shù)。

這個(gè)小小工程中,包含了#include頭文件包含、#define宏定義、#ifdef條件編譯等幾個(gè)重要的預(yù)處理指令,我認(rèn)為,稍微有一點(diǎn)點(diǎn)C語(yǔ)言基礎(chǔ)的朋友都應(yīng)該可以毫無(wú)障礙地看懂這幾行代碼。

3.3 預(yù)編譯生成.i文件

預(yù)編譯是編譯流程的第一步,這里最重點(diǎn)就是預(yù)處理指令的處理。

使用gcc編譯器執(zhí)行預(yù)編譯操作,需要用到的主要命令行參數(shù)是-E,具體如下:

gcc -E main.c -o main.i
gcc -E sub.c -o sub.i

注意:這里是每一個(gè).c源文件都需要預(yù)編譯,-o表示指定生成預(yù)編譯后的文件名稱(chēng),一般這個(gè)文件我們使用.i后綴。

為了了解預(yù)編譯究竟干了啥?我們可以打開(kāi)這些.i文件,一瞧究竟。這里以main.i為例,我們來(lái)看看:

# 1 "main.c"
# 1 ""
# 1 ""
# 31 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "" 2
# 1 "main.c"

# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 461 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 452 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 453 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
# 454 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 462 "/usr/include/features.h" 2 3 4
# 485 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 486 "/usr/include/features.h" 2 3 4
# 34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4


/* 篇幅有限,中間省略了內(nèi)容 */


extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 840 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));



extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 858 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 873 "/usr/include/stdio.h" 3 4

# 3 "main.c" 2

# 1 "sub.h" 1







# 7 "sub.h"
extern int sub_func(int a);
# 5 "main.c" 2
# 13 "main.c"
int main(int argc, const char *argv[])
{
 printf("Hello world !\n");
 printf("TEST_NUM = %d\n", 1024);
 printf("sub_func() = %d\n", sub_func(1));





 return 0;
}

就算在不了解預(yù)編譯原理的情況下,我們也可以清晰地發(fā)現(xiàn),一個(gè)20來(lái)行的.c源文件,被生成了一個(gè)700多行的.i預(yù)編譯處理文件。

為何會(huì)多了那么行呢?仔細(xì)對(duì)比你會(huì)發(fā)現(xiàn),其實(shí)main.i就是把stdio.hsub.h這兩個(gè)頭文件中除去#開(kāi)頭的預(yù)處理之后的那些內(nèi)容給搬過(guò)來(lái)了,這就是#include的作用。

值得提一點(diǎn)的就是,這個(gè).i文件中還是有# xxx這種信息存在,其實(shí)這個(gè)信息是有作用的,下篇講解決編譯問(wèn)題的實(shí)戰(zhàn)時(shí),再重點(diǎn)介紹下它的作用。

這里,我再介紹一個(gè)gcc的參數(shù),可以去掉這些信息,讓.i文件看起來(lái)清爽一些。

這個(gè)參數(shù)就是-P(注意:大寫(xiě)字母P),這個(gè)參數(shù)在gcc--help里面沒(méi)有介紹,需要問(wèn)一下男人man

gcc/gcc_helloworld$ man gcc | grep -w '\-P'
           file -M  -MD  -MF  -MG  -MM  -MMD  -MP  -MQ  -MT -no-integrated-cpp  -P  -pthread  -remap -traditional
           inhibited with the negated form -fno-working-directory.  If the -P flag is present in the command line, this option
       -P  Inhibit generation of linemarkers in the output from the preprocessor.  This might be useful when running the
troff: :17361: warning [p 110, 20.7i]: can't break line

加上-P參數(shù)之后,預(yù)編譯出來(lái)的main.i文件就清爽多了,一下子就減少到200多行了。

typedef long unsigned int size_t;
typedef __builtin_va_list __gnuc_va_list;
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;
typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;
typedef __int8_t __int_least8_t;
typedef __uint8_t __uint_least8_t;
typedef __int16_t __int_least16_t;
typedef __uint16_t __uint_least16_t;
typedef __int32_t __int_least32_t;
typedef __uint32_t __uint_least32_t;
typedef __int64_t __int_least64_t;
typedef __uint64_t __uint_least64_t;
typedef long int __quad_t;
typedef long int __blksize_t;
typedef long int __blkcnt_t;
typedef long int __blkcnt64_t;
typedef __off64_t __loff_t;
typedef char *__caddr_t;
typedef long int __intptr_t;
typedef unsigned int __socklen_t;
typedef int __sig_atomic_t;

/* 篇幅有限,中間省略了內(nèi)容 */

extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);

extern int sub_func(int a);
int main(int argc, const char *argv[])
{
 printf("Hello world !\n");
 printf("TEST_NUM = %d\n", 1024);
 printf("sub_func() = %d\n", sub_func(1));
 return 0;
}

3.4 編譯生成.s文件

預(yù)編譯處理完了之后,進(jìn)入到編譯階段,這里需要做到就是語(yǔ)法檢查和詞法分析,最終是會(huì)生成匯編代碼,我們一般以.s后綴表示此類(lèi)文件。

以gcc編譯器為例,執(zhí)行這一步編譯用到的命令行參數(shù)是-S大寫(xiě)字母S),具體如下:

gcc -S main.i -o main.s
gcc -S sub.i -o sub.s

.i文件一樣,以main.s為例,我們也可以打開(kāi)它,看下它里面長(zhǎng)啥樣?

    .file   "main.c"
    .text
    .section    .rodata
.LC0:
    .string "Hello world !"
.LC1:
    .string "TEST_NUM = %d\n"
.LC2:
    .string "sub_func() = %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movq    %rsi, -16(%rbp)
    leaq    .LC0(%rip), %rdi
    call    puts@PLT
    movl    $1024, %esi
    leaq    .LC1(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $1, %edi
    call    sub_func@PLT
    movl    %eax, %esi
    leaq    .LC2(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long    1f - 0f
    .long    4f - 1f
    .long    5
0:
    .string  "GNU"
1:
    .align 8
    .long    0xc0000002
    .long    3f - 2f
2:
    .long    0x3
3:
    .align 8
4:

有過(guò)匯編語(yǔ)言基礎(chǔ)的朋友,一定不會(huì)陌生:“咦,這不就是我們?cè)?strong>匯編語(yǔ)言編程課堂上手把手碼出來(lái)的匯編代碼嗎?”

是的,這個(gè)就是純匯編代碼,它的可讀性比C語(yǔ)言確實(shí)差了很多,這也從側(cè)面證實(shí)了gcc這類(lèi)C編譯器的厲害之處,它可以把高級(jí)語(yǔ)言編寫(xiě)的C代碼編譯成面向機(jī)器的低級(jí)語(yǔ)言的匯編代碼。

3.5 匯編生成.o文件

生成匯編代碼之后,接下來(lái)的步驟就是使用匯編器生成二進(jìn)制目標(biāo)文件,這里使用gcc匯編的命令行如下:

  1. gcc -c main.s -o main.o
  2. gcc -c sub.s -o sub.o

同樣的,你是否也好奇,.o這種目標(biāo)文件究竟長(zhǎng)啥樣?以main.o,我們來(lái)看一看?

image-20211203145755275

額,忘了再特別交代下,這貨是二進(jìn)制文件,它并不像.c、.i.s文件那樣是可讀的,我一使用cat指令去讀,直接把我的控制臺(tái)輸出都給整亂碼了。(< - . - >)

看來(lái),這玩意真不是我們普通肉眼所能看得懂的。

但是,Linux這么多強(qiáng)大的命令行,cat不能解析它,自然有人能敲開(kāi)它的大門(mén),這次我們用下面這兩個(gè)命令簡(jiǎn)單看看這個(gè)目標(biāo)文件。

使用file命令先查看下,文件的類(lèi)型:

gcc/gcc_helloworld$ file main.c
main.c: C source, ASCII text
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ file main.i
main.i: C source, ASCII text
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ file main.s
main.s: assembler source, ASCII text
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

我們可以清晰地對(duì)比到不同的文件類(lèi)型,以及obj文件在Linux平臺(tái)上其實(shí)是一個(gè)ELF文件。

再使用nm命令查看下目標(biāo)文件的符號(hào)列表:

gcc/gcc_helloworld$ nm -a main.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 r .eh_frame
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
0000000000000000 a main.c
0000000000000000 r .note.gnu.property
0000000000000000 n .note.GNU-stack
                 U printf
                 U puts
0000000000000000 r .rodata
                 U sub_func
0000000000000000 t .text

這里補(bǔ)充一下:

Tt : 表示該符號(hào)是在本C文件中實(shí)現(xiàn)的函數(shù)(符號(hào));U: 表示該符號(hào)是外部符號(hào),也就是在其他C文件中實(shí)現(xiàn)的;

nm更為詳細(xì)的含義列表,感興趣的可以自行man nm

nm的輸出,可以看出符號(hào)列表跟我們的C代碼實(shí)現(xiàn)是吻合的。

3.6 預(yù)編譯生成.elf文件

所有的目標(biāo)文件生成后,編譯流程進(jìn)入到鏈接階段。

這一步需要做的就是所有生成的二進(jìn)制目標(biāo)文件、啟動(dòng)代碼、依賴(lài)的庫(kù)文件,一并鏈接成一個(gè)可執(zhí)行文件,這個(gè)可執(zhí)行文件可被加載或拷貝到存儲(chǔ)器中去執(zhí)行。

在Linux下,可執(zhí)行文件的本質(zhì)是一個(gè)elf文件,全稱(chēng)是:Executable and Linkable Format,中文含義就是:可執(zhí)行、可鏈接的格式文件。

我們來(lái)看下,使用gcc命令行如何生成.elf文件的,如下:

gcc main.o sub.o -o test

由于gcc強(qiáng)大的默認(rèn)選項(xiàng),我們?cè)谳斎氲臅r(shí)候,只需要輸入我們的目標(biāo)文件列表,以及使用-o指定輸出的可執(zhí)行文件名稱(chēng)即可。

其實(shí)它真正在鏈接的時(shí)候是會(huì)加入很多其他文件(啟動(dòng)文件、庫(kù)文件等等)和選項(xiàng)的,針對(duì)這個(gè)問(wèn)題,下文我特意留了一個(gè)疑問(wèn)。

總之,經(jīng)過(guò)這一步之后,一個(gè)elf可執(zhí)行文件就生成了,在Linux平臺(tái)上,通過(guò)./test就可以運(yùn)行我們編寫(xiě)的C代碼了。

gcc/gcc_helloworld$ ./test 
Hello world !
TEST_NUM = 1024
sub_func() = 2

執(zhí)行的輸出,與我們之前設(shè)計(jì)的代碼邏輯也是保持一致的。

同樣的,我們也使用filenm命令查看下這個(gè)test可執(zhí)行文件:

gcc/gcc_helloworld$ file test 
test: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2b10713c6b777b4201108c59c41547baffeb9abc, for GNU/Linux 3.2.0, not stripped
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ nm -a test
0000000000000000 a 
0000000000004010 b .bss
0000000000004010 B __bss_start
0000000000000000 n .comment
0000000000004010 b completed.8060
0000000000000000 a crtstuff.c
0000000000000000 a crtstuff.c
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 d .data
0000000000004000 D __data_start
0000000000004000 W data_start
00000000000010b0 t deregister_tm_clones
0000000000001120 t __do_global_dtors_aux
0000000000003db8 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003dc0 d .dynamic
0000000000003dc0 d _DYNAMIC
0000000000000488 r .dynstr
00000000000003c8 r .dynsym
0000000000004010 D _edata
0000000000002080 r .eh_frame
0000000000002034 r .eh_frame_hdr
0000000000004018 B _end
0000000000001258 t .fini
0000000000001258 T _fini
0000000000003db8 d .fini_array
0000000000001160 t frame_dummy
0000000000003db0 d __frame_dummy_init_array_entry
00000000000021a4 r __FRAME_END__
0000000000003fb0 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002034 r __GNU_EH_FRAME_HDR
00000000000003a0 r .gnu.hash
0000000000000512 r .gnu.version
0000000000000528 r .gnu.version_r
0000000000003fb0 d .got
0000000000001000 t .init
0000000000001000 t _init
0000000000003db0 d .init_array
0000000000003db8 d __init_array_end
0000000000003db0 d __init_array_start
0000000000000318 r .interp
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001250 T __libc_csu_fini
00000000000011e0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000001169 T main
0000000000000000 a main.c
000000000000037c r .note.ABI-tag
0000000000000358 r .note.gnu.build-id
0000000000000338 r .note.gnu.property
0000000000001020 t .plt
0000000000001050 t .plt.got
0000000000001060 t .plt.sec
                 U printf@@GLIBC_2.2.5
                 U puts@@GLIBC_2.2.5
00000000000010e0 t register_tm_clones
0000000000000548 r .rela.dyn
0000000000000608 r .rela.plt
0000000000002000 r .rodata
0000000000001080 T _start
0000000000000000 a sub.c
00000000000011c2 T sub_func
0000000000001080 t .text
0000000000004010 D __TMC_END__

對(duì)比之前的main.o,它的文件類(lèi)型描述中多了一些信息,查看的符號(hào)列表中也多了很多沒(méi)見(jiàn)過(guò)的符號(hào),這些符號(hào)是因?yàn)橐蕾?lài)的系統(tǒng)庫(kù)和啟動(dòng)文件而導(dǎo)進(jìn)來(lái)的。

3.7 轉(zhuǎn)換生成.bin文件

如上面章節(jié)提及的那樣,資源緊張的嵌入式設(shè)備,如果跑到不是嵌入式Linux系統(tǒng),那么是不可能直接跑.elf這種可執(zhí)行文件的。

大部分內(nèi)存只有百來(lái)KB的嵌入式設(shè)備,是無(wú)法支持可執(zhí)行文件的解析的,所以我們就需要在編譯生成elf文件之后,將elf文件轉(zhuǎn)換成bin文件,再把bin文件燒錄到Flash中運(yùn)行代碼。

這一步,在Linux平臺(tái),我們使用的是objcopy命令,使用如下:

  1. objcopy -O binary test test.bin

這里-O(大寫(xiě)字母O)是用于指定輸出二進(jìn)制內(nèi)容,它還可以支持ihex等參數(shù),具體可以man objcopy。

這個(gè)test.bin的文件類(lèi)型以及顯示的內(nèi)容如下所示,毫無(wú)疑問(wèn),它也是二進(jìn)制的不可讀。

image-20211203160804650

3.8 all-in-one

有了上面的各個(gè)步驟的基礎(chǔ),從預(yù)編譯、編譯、匯編,再到鏈接,每次都需要給gcc輸入不同的參數(shù),有點(diǎn)麻煩呀?

那么有沒(méi)有參數(shù)可以輸入一次就可以獲取到這些步驟的所有輸出文件?。?/p>

巧了,gcc還真有!這個(gè)參數(shù)就是-save-temps=obj,我們來(lái)實(shí)踐下:

gcc/gcc_helloworld$ ./build.sh clean   
Clean build done !
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ ls
build.sh  main.c  README.md  sub.c  sub.h
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ ./build.sh allinone
gcc -c main.c -o main.o -save-temps=obj
gcc -c sub.c -o sub.o -save-temps=obj
gcc main.o sub.o -o test
gcc/gcc_helloworld$ 
gcc/gcc_helloworld$ ls
build.sh  main.c  main.i  main.o  main.s  README.md  sub.c  sub.h  sub.i  sub.o  sub.s  test

就這樣,.i文件.s文件、以及.o文件都同時(shí)輸出來(lái)了。

如果工程中,只有一個(gè)main.c的源文件的話(huà),還可以這樣就一步搞定。

gcc main.c -o test -save-temps=obj

這些.i文件.s文件、以及.o文件,我們稱(chēng)之為中間臨時(shí)文件,下篇介紹如何解決一些編譯相關(guān)的問(wèn)題,還得好好利用這些中間臨時(shí)文件呢。

4 經(jīng)驗(yàn)總結(jié)

  • C代碼編譯要經(jīng)過(guò)預(yù)編譯、編譯、匯編、鏈接這幾步,每一步做的事情是不一樣的;
  • 要深入了解C代碼的編譯流程,建議摒棄Windows下的IDE編譯器,那玩意除了提高你的編碼速度,對(duì)你理解編譯流程和編譯原理,幫助并不大;
  • gcc是一個(gè)開(kāi)源的C編譯器,它博大精深,支持一大堆的命令行參數(shù),了解一些基礎(chǔ)、常用的參數(shù),對(duì)你理解問(wèn)題幫助很大;
  • 資源受限的嵌入式設(shè)備往往跑的是RTOS,這樣的執(zhí)行環(huán)境下,往往只能燒錄bin文件到Flash中,而不支持像高級(jí)操作系統(tǒng)那樣,直接加載可執(zhí)行文件到內(nèi)存中運(yùn)行。

5 留個(gè)疑問(wèn)

gcc怎么這么牛逼?

好像啥事都能干?

從命令行上看,gcc既能預(yù)處理,也能編譯C代碼,又可以執(zhí)行匯編ASM代碼,還能鏈接OBJ目標(biāo)文件生成可執(zhí)行文件,這里面的操作真的只是gcc在干活嗎?

感興趣的朋友,可以關(guān)注下這個(gè)疑問(wèn),后面有時(shí)間把gcc相關(guān)的內(nèi)幕補(bǔ)上。

6 更多分享

本項(xiàng)目的所有測(cè)試代碼和編譯腳本,均可以在我的github倉(cāng)庫(kù)01workstation中找到。

歡迎關(guān)注我的github倉(cāng)庫(kù)01workstation,日常分享一些開(kāi)發(fā)筆記和項(xiàng)目實(shí)戰(zhàn),歡迎指正問(wèn)題。

同時(shí)也非常歡迎關(guān)注我的CSDN主頁(yè)和專(zhuān)欄:

【CSDN主頁(yè):架構(gòu)師李肯】

RT-Thread主頁(yè):架構(gòu)師李肯】

【C/C++語(yǔ)言編程專(zhuān)欄】

【GCC專(zhuān)欄】

【信息安全專(zhuān)欄】

【RT-Thread開(kāi)發(fā)筆記】

freeRTOS開(kāi)發(fā)筆記】

有問(wèn)題的話(huà),可以跟我討論,知無(wú)不答,謝謝大家。

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • GCC
    GCC
    +關(guān)注

    關(guān)注

    0

    文章

    105

    瀏覽量

    24781
  • C代碼
    +關(guān)注

    關(guān)注

    1

    文章

    89

    瀏覽量

    14237
  • C文件
    +關(guān)注

    關(guān)注

    0

    文章

    12

    瀏覽量

    2820
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    C語(yǔ)言變成可執(zhí)行文件的四大步驟

    C語(yǔ)言變成最終的可執(zhí)行文件,需要經(jīng)過(guò)四步。
    發(fā)表于 10-18 10:37 ?5142次閱讀

    Linux 下GCC編譯

    一、Linux 下多文件編譯 在上一篇 Linux 下的 C 編程我們知道了 Linux 下的編譯器為 GCC ,以及如何使用
    的頭像 發(fā)表于 09-11 15:18 ?2078次閱讀
    Linux 下<b class='flag-5'>GCC</b>的<b class='flag-5'>編譯</b>

    常用編輯器之GCC編譯

    ,生成可執(zhí)行文件。我們可以通過(guò)一個(gè)簡(jiǎn)單的hello.c程序的編譯過(guò)程對(duì)GCC的整個(gè)編譯過(guò)程有一個(gè)簡(jiǎn)單的了解。1)預(yù)處理elf@ubuntu:
    發(fā)表于 08-24 11:05

    嵌入式學(xué)習(xí)-常用編輯器之GCC編譯

    -o hello_ubuntu 可以看到,hello.c文件編譯成功,生成可執(zhí)行文件hello_ubuntu,我們?cè)诮K端運(yùn)行./hello_ubuntu,可以看到輸出結(jié)果和我們程
    發(fā)表于 08-27 10:17

    gcc編譯通過(guò)但是arm-linux-gcc不能編譯,以及如何下載文件到arm

    編譯程序而且的確運(yùn)行打印出來(lái) “nihao”(圖5)但是當(dāng)我輸入arm-linux-gcc –g –o 試圖形成arm可執(zhí)行文件出現(xiàn)以下錯(cuò)誤。(圖6) 還有個(gè)問(wèn)題就是這一步我成功后,要下載到arm
    發(fā)表于 11-02 10:57

    使用gcc編譯命令

    都會(huì)將a.c和b.c編譯。無(wú)論兩個(gè)C文件的內(nèi)容是否修改過(guò)。我們很容易想到,萬(wàn)一某個(gè)工程有許多文件
    發(fā)表于 12-17 07:45

    【原創(chuàng)精選】RT-Thread征文精選技術(shù)文章合集

    編譯優(yōu)化系列】使用GCC如何C文件
    發(fā)表于 07-26 14:56

    請(qǐng)問(wèn)運(yùn)行在RK3588板上編譯可執(zhí)行文件出現(xiàn)的問(wèn)題該怎么解決?

    gcc-buildroot-9.3.0-2020.03-x86_64_aarch64-rockchip-linux-gnu,我理解是交叉編譯器,應(yīng)該在Ubuntu主機(jī)上使用,無(wú)法在開(kāi)發(fā)板使用在開(kāi)發(fā)板上直接編譯正常,但
    發(fā)表于 01-10 14:28

    用MDK生成bin格式的可執(zhí)行文件

    用MDK 生成bin 文件1用MDK 生成bin 文件Embest 徐良平在RV MDK 中,默認(rèn)情況下生成*.hex 的可執(zhí)行文件,但是當(dāng)我們要生成*.bin 的可執(zhí)行文件時(shí)怎么辦呢
    發(fā)表于 08-02 10:52 ?71次下載

    了解在Linux下可執(zhí)行文件格式

    Linux下面,目標(biāo)文件、共享對(duì)象文件、可執(zhí)行文件都是使用ELF文件格式來(lái)存儲(chǔ)的。程序經(jīng)過(guò)編譯之后會(huì)輸出目標(biāo)
    發(fā)表于 05-15 08:49 ?1868次閱讀

    Linux下可執(zhí)行文件格式

    Linux支持的可執(zhí)行文件主要有:Coff,elf,flat,類(lèi)似Windows的.exeCoff文件格式? Common Object File Format,最早與uclinux
    發(fā)表于 04-02 14:46 ?1489次閱讀

    GCC編譯C語(yǔ)言程序的過(guò)程是怎么樣的

    使用GCCC語(yǔ)言源代碼文件生成可執(zhí)行文件的過(guò)程,需要經(jīng)歷四個(gè)的步驟:預(yù)處理(Preprocessing)編譯(Compilation)匯編
    的頭像 發(fā)表于 02-18 11:47 ?3886次閱讀

    gcc編譯優(yōu)化系列】如何獲取gcc默認(rèn)的鏈接腳本

    我們都知道在一般的嵌入式開(kāi)發(fā)中,使用gcc編譯固件的一般流程是,先把所有的.c文件和.s文件編譯成
    的頭像 發(fā)表于 07-11 09:15 ?3055次閱讀

    單獨(dú)下載可執(zhí)行文件到MM32F5微控制器

    使用Keil MDK或者IAR等使用圖形界面的開(kāi)發(fā)環(huán)境,可以在圖形界面環(huán)境下編譯源碼工程,并下載編譯生成的可執(zhí)行文件到目標(biāo)微控制器中。但若使用ARMGCC等命令行工具鏈,需要額外的下載工具,才能將
    的頭像 發(fā)表于 02-17 09:32 ?766次閱讀

    單獨(dú)下載可執(zhí)行文件到MM32F5微控制器

    使用Keil MDK或者IAR等使用圖形界面的開(kāi)發(fā)環(huán)境,可以在圖形界面環(huán)境下編譯源碼工程,并下載編譯生成的可執(zhí)行文件到目標(biāo)微控制器中。
    的頭像 發(fā)表于 05-24 17:24 ?1357次閱讀
    單獨(dú)下載<b class='flag-5'>可執(zhí)行文件</b>到MM32F5微控制器