RM新时代网站-首页

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

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

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

你知道你寫的代碼是怎樣跑起來的嗎(上)

jf_78858299 ? 來源:開發(fā)內(nèi)功修煉 ? 作者:開發(fā)內(nèi)功修煉 ? 2023-05-05 14:36 ? 次閱讀

今天我們來思考一個簡單的問題,一個程序是如何在 Linux 上執(zhí)行起來的?

我們就拿全宇宙最簡單的 Hello World 程序來舉例。

#include 
int main()
{
   printf("Hello, World!\\n");
   return 0;
}

我們在寫完代碼后,進行簡單的編譯,然后在 shell 命令行下就可以把它啟動起來。

# gcc main.c -o helloworld
# ./helloworld
Hello, World!

那么在編譯啟動運行的過程中都發(fā)生了哪些事情了呢?今天就讓我們來深入地了解一下。

一、理解可執(zhí)行文件格式

源代碼在編譯后會生成一個可執(zhí)行程序文件,我們先來了解一下編譯后的二進制文件是什么樣子的。

我們首先使用 file 命令查看一下這個文件的格式。

# file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ...

file 命令給出了這個二進制文件的概要信息,其中 ELF 64-bit LSB executable 表示這個文件是一個 ELF 格式的 64 位的可執(zhí)行文件。x86-64 表示該可執(zhí)行文件支持的 cpu 架構(gòu)。

LSB 的全稱是 Linux Standard Base,是 Linux 標準規(guī)范。其目的是制定一系列標準來增強 Linux 發(fā)行版的兼容性。

ELF 的全稱是 Executable Linkable Format,是一種二進制文件格式。Linux 下的目標文件、可執(zhí)行文件和 CoreDump 都按照該格式進行存儲。

ELF 文件由四部分組成,分別是 ELF 文件頭 (ELF header)、Program header table、Section 和 Section header table。

圖片

接下來我們分幾個小節(jié)挨個介紹一下。

1.1 ELF 文件頭

ELF 文件頭記錄了整個文件的屬性信息。原始二進制非常不便于觀察。不過我們有趁手的工具 - readelf,這個工具可以幫我們查看 ELF 文件中的各種信息。

我們先來看一下編譯出來的可執(zhí)行文件的 ELF 文件頭,使用 --file-header (-h) 選項即可查看。

# readelf --file-header helloworld
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401040
  Start of program headers:          64 (bytes into file)
  Start of section headers:          23264 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

ELF 文件頭包含了當前可執(zhí)行文件的概要信息,我把其中關(guān)鍵的幾個拿出來給大家解釋一下。

  • Magic:一串特殊的識別碼,主要用于外部程序快速地對這個文件進行識別,快速地判斷文件類型是不是 ELF
  • Class:表示這是 ELF64 文件
  • Type:為 EXEC 表示是可執(zhí)行文件,其它文件類型還有 REL(可重定位的目標文件)、DYN(動態(tài)鏈接庫)、CORE(系統(tǒng)調(diào)試 coredump文件)
  • Entry point address:程序入口地址,這里顯示入口在 0x401040 位置處
  • Size of this header:ELF 文件頭的大小,這里顯示是占用了 64 字節(jié)

以上幾個字段是 ELF 頭中對 ELF 的整體描述。另外 ELF 頭中還有關(guān)于 program headers 和 section headers 的描述信息。

  • Start of program headers:表示 Program header 的位置
  • Size of program headers:每一個 Program header 大小
  • Number of program headers:總共有多少個 Program header
  • Start of section headers: 表示 Section header 的開始位置。
  • Size of section headers:每一個 Section header 的大小
  • Number of section headers: 總共有多少個 Section header

1.2 Program Header Table

在介紹 Program Header Table 之前我們展開介紹一下 ELF 文件中一對兒相近的概念 - Segment 和 Section。

ELF 文件內(nèi)部最重要的組成單位是一個一個的 Section。每一個 Section 都是由編譯鏈接器生成的,都有不同的用途。例如編譯器會將我們寫的代碼編譯后放到 .text Section 中,將全局變量放到 .data 或者是 .bss Section中。

但是對于操作系統(tǒng)來說,它不關(guān)注具體的 Section 是啥,它只關(guān)注這塊內(nèi)容應(yīng)該以何種權(quán)限加載到內(nèi)存中,例如讀,寫,執(zhí)行等權(quán)限屬性。因此相同權(quán)限的 Section 可以放在一起組成 Segment,以方便操作系統(tǒng)更快速地加載。

由于 Segment 和 Section 翻譯成中文的話,意思太接近了,非常不利于理解。所以本文中我就直接使用 Segment 和 Section 原汁原味的概念,而不是將它們翻譯成段或者是節(jié),這樣太容易讓人混淆了。

Program headers table 就是作為所有 Segments 的頭信息,用來描述所有的 Segments 的。 。

使用 readelf 工具的 --program-headers(-l)選項可以解析查看到這塊區(qū)域里存儲的內(nèi)容。

# readelf --program-headers helloworld
Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
     FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
     0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
     0x000000000000001c 0x000000000000001c  R      0x1
   [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
     0x0000000000000438 0x0000000000000438  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
     0x00000000000001c5 0x00000000000001c5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
     0x0000000000000138 0x0000000000000138  R      0x1000
  LOAD           0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
     0x0000000000000220 0x0000000000000228  RW     0x1000
  DYNAMIC        0x0000000000002e20 0x0000000000403e20 0x0000000000403e20
     0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x00000000000002c4 0x00000000004002c4 0x00000000004002c4
     0x0000000000000044 0x0000000000000044  R      0x4
  GNU_EH_FRAME   0x0000000000002014 0x0000000000402014 0x0000000000402014
     0x000000000000003c 0x000000000000003c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
     0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
     0x00000000000001f0 0x00000000000001f0  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   06     .dynamic 
   07     .note.gnu.build-id .note.ABI-tag 
   08     .eh_frame_hdr 
   09     
   10     .init_array .fini_array .dynamic .got

上面的結(jié)果顯示總共有 11 個 program headers。

對于每一個段,輸出了 Offset、VirtAddr 等描述當前段的信息。Offset 表示當前段在二進制文件中的開始位置,F(xiàn)ileSiz 表示當前段的大小。Flag 表示當前的段的權(quán)限類型, R 表示可都、E 表示可執(zhí)行、W 表示可寫。

在最下面,還把每個段是由哪幾個 Section 組成的給展示了出來,比如 03 號段是由“.init .plt .text .fini” 四個 Section 組成的。

1.3 Section Header Table

和 Program Header Table 不一樣的是,Section header table 直接描述每一個 Section。這二者描述的其實都是各種 Section ,只不過目的不同,一個針對加載,一個針對鏈接。

使用 readelf 工具的 --section-headers (-S)選項可以解析查看到這塊區(qū)域里存儲的內(nèi)容。

# readelf --section-headers helloworld
There are 30 section headers, starting at offset 0x5b10:

Section Headers:
  [Nr] Name              Type             Address           Offset
    Size              EntSize          Flags  Link  Info  Align
  ......
  [13] .text             PROGBITS         0000000000401040  00001040
    0000000000000175  0000000000000000  AX       0     0     16
  ......
  [23] .data             PROGBITS         0000000000404020  00003020
    0000000000000010  0000000000000000  WA       0     0     8
  [24] .bss              NOBITS           0000000000404030  00003030
    0000000000000008  0000000000000000  WA       0     0     1
  ......    
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

結(jié)果顯示,該文件總共有 30 個 Sections,每一個 Section 在二進制文件中的位置通過 Offset 列表示了出來。Section 的大小通過 Size 列體現(xiàn)。

在這 30 個Section中,每一個都有獨特的作用。我們編寫的代碼在編譯成二進制指令后都會放到 .text 這個 Section 中。另外我們看到 .text 段的 Address 列顯示的地址是 0000000000401040?;貞浨懊嫖覀冊?ELF 文件頭中看到 Entry point address 顯示的入口地址為 0x401040。這說明,程序的入口地址就是 .text 段的地址。

另外還有兩個值得關(guān)注的 Section 是 .data 和 .bss。代碼中的全局變量數(shù)據(jù)在編譯后將在在這兩個 Section 中占據(jù)一些位置。如下簡單代碼所示。

//未初始化的內(nèi)存區(qū)域位于 .bss 段
int data1 ;     

//已經(jīng)初始化的內(nèi)存區(qū)域位于 .data 段
int data2 = 100 ;  

//代碼位于 .text 段
int main(void)
{
 ...
}

1.4 入口進一步查看

接下來,我們想再查看一下我們前面提到的程序入口 0x401040,看看它到底是啥。我們這次再借助 nm 命令來進一步查看一下可執(zhí)行文件中的符號及其地址信息。-n 選項的作用是顯示的符號以地址排序,而不是名稱排序。

# nm -n helloworld
     w __gmon_start__
     U __libc_start_main@@GLIBC_2.2.5
     U printf@@GLIBC_2.2.5
......                 
0000000000401040 T _start
......
0000000000401126 T main

通過以上輸出可以看到,程序入口 0x401040 指向的是 _start 函數(shù)的地址,在這個函數(shù)執(zhí)行一些初始化的操作之后,我們的入口函數(shù) main 將會被調(diào)用到,它位于 0x401126 地址處。

二、用戶進程的創(chuàng)建過程概述

在我們編寫的代碼編譯完生成可執(zhí)行程序之后,下一步就是使用 shell 把它加載起來并運行之。一般來說 shell 進程是通過fork+execve來加載并運行新進程的。一個簡單加載 helloworld 命令的 shell 核心邏輯是如下這個過程。

// shell 代碼示例
int main(int argc, char * argv[])
{
 ...
 pid = fork();
 if (pid==0){ // 如果是在子進程中
  //使用 exec 系列函數(shù)加載并運行可執(zhí)行文件
  execve("helloworld", argv, envp);
 } else {
  ...
 }
 ...
}

shell 進程先通過 fork 系統(tǒng)調(diào)用創(chuàng)建一個進程出來。然后在子進程中調(diào)用 execve 將執(zhí)行的程序文件加載起來,然后就可以調(diào)到程序文件的運行入口處運行這個程序了。

在上一篇文章[《Linux進程是如何創(chuàng)建出來的?》]中,我們詳細介紹過了 fork 的工作過程。這里我們再簡單過一下。

這個 fork 系統(tǒng)調(diào)用在內(nèi)核入口是在 kernel/fork.c 下。

//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
 return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}

在 do_fork 的實現(xiàn)中,核心是一個 copy_process 函數(shù),它以拷貝父進程(線程)的方式來生成一個新的 task_struct 出來。

//file:kernel/fork.c
long do_fork(...)
{
 //復制一個 task_struct 出來
 struct task_struct *p;
 p = copy_process(clone_flags, stack_start, stack_size,
    child_tidptr, NULL, trace);

 //子任務(wù)加入到就緒隊列中去,等待調(diào)度器調(diào)度
 wake_up_new_task(p);
 ...
}

在 copy_process 函數(shù)中為新進程申請 task_struct,并用當前進程自己的地址空間、命名空間等對新進程進行初始化,并為其申請進程 pid。

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
 //復制進程 task_struct 結(jié)構(gòu)體
 struct task_struct *p;
 p = dup_task_struct(current);
 ...

 //進程核心元素初始化
 retval = copy_files(clone_flags, p);
 retval = copy_fs(clone_flags, p);
 retval = copy_mm(clone_flags, p);
 retval = copy_namespaces(clone_flags, p);
 ...

 //申請 pid && 設(shè)置進程號
 pid = alloc_pid(p->nsproxy->pid_ns);
 p->pid = pid_nr(pid);
 p->tgid = p->pid;
 ......
}

執(zhí)行完后,進入 wake_up_new_task 讓新進程等待調(diào)度器調(diào)度。

不過 fork 系統(tǒng)調(diào)用只能是根據(jù)當?shù)?shell 進程再復制一個新的進程出來。這個新進程里的代碼、數(shù)據(jù)都還是和原來的 shell 進程的內(nèi)容一模一樣。

要想實現(xiàn)加載并運行另外一個程序,比如我們編譯出來的 helloworld 程序,那還需要使用到 execve 系統(tǒng)調(diào)用。

三. Linux 可執(zhí)行文件加載器

其實 Linux 不是寫死只能加載 ELF 一種可執(zhí)行文件格式的。它在啟動的時候,會把自己支持的所有可執(zhí)行文件的解析器都加載上。并使用一個 formats 雙向鏈表來保存所有的解析器。其中 formats 雙向鏈表在內(nèi)存中的結(jié)構(gòu)如下圖所示。

我們就以 ELF 的加載器 elf_format 為例,來看看這個加載器是如何注冊的。在 Linux 中每一個加載器都用一個 linux_binfmt 結(jié)構(gòu)來表示。其中規(guī)定了加載二進制可執(zhí)行文件的 load_binary 函數(shù)指針,以及加載崩潰文件 的 core_dump 函數(shù)等。其完整定義如下

//file:include/linux/binfmts.h
struct linux_binfmt {
 ...
 int (*load_binary)(struct linux_binprm *);
 int (*load_shlib)(struct file *);
 int (*core_dump)(struct coredump_params *cprm);
};

其中 ELF 的加載器 elf_format 中規(guī)定了具體的加載函數(shù),例如 load_binary 成員指向的就是具體的 load_elf_binary 函數(shù)。這就是 ELF 加載的入口。

//file:fs/binfmt_elf.c
static struct linux_binfmt elf_format = {
 .module  = THIS_MODULE,
 .load_binary = load_elf_binary,
 .load_shlib = load_elf_library,
 .core_dump = elf_core_dump,
 .min_coredump = ELF_EXEC_PAGESIZE,
};

加載器 elf_format 會在初始化的時候通過 register_binfmt 進行注冊。

//file:fs/binfmt_elf.c
static int __init init_elf_binfmt(void)
{
 register_binfmt(&elf_format);
 return 0;
}

而 register_binfmt 就是將加載器掛到全局加載器列表 - formats 全局鏈表中。

//file:fs/exec.c
static LIST_HEAD(formats);

void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
 ...
 insert ? list_add(&fmt->lh, &formats) :
   list_add_tail(&fmt->lh, &formats);
}

Linux 中除了 elf 文件格式以外還支持其它格式,在源碼目錄中搜索 register_binfmt,可以搜索到所有 Linux 操作系統(tǒng)支持的格式的加載程序。

# grep -r "register_binfmt" *
fs/binfmt_flat.c: register_binfmt(&flat_format);
fs/binfmt_elf_fdpic.c: register_binfmt(&elf_fdpic_format);
fs/binfmt_som.c: register_binfmt(&som_format);
fs/binfmt_elf.c: register_binfmt(&elf_format);
fs/binfmt_aout.c: register_binfmt(&aout_format);
fs/binfmt_script.c: register_binfmt(&script_format);
fs/binfmt_em86.c: register_binfmt(&em86_format);

將來在 Linux 在加載二進制文件時會遍歷 formats 鏈表,根據(jù)要加載的文件格式來查詢合適的加載器。

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

    關(guān)注

    87

    文章

    11292

    瀏覽量

    209323
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4779

    瀏覽量

    68521
  • helloworld
    +關(guān)注

    關(guān)注

    0

    文章

    13

    瀏覽量

    4365
收藏 人收藏

    評論

    相關(guān)推薦

    MotorControl Workbench生成的代碼是開環(huán)的嗎,為什么電機跑起來很容易受到外力導致停機?

    請問各位高手 MotorControl Workbench 生成的代碼是開環(huán)的嗎?為什么我的電機跑起來很容易受到外力導致停機,我想讓它不停機,請問有什么好的辦法嗎 ?
    發(fā)表于 03-21 07:12

    請問HVMotorCtrl+PfcKit_v1.7/HVPM_sensorless_2833x代碼能不能讓電機跑起來?需要修改哪些參數(shù)?

    ,現(xiàn)在想測試一下代碼能不能讓電機跑起來,從level1——level6,不知道從哪個level可以讓電機跑起來,聽說比較危險,不知道需要改什
    發(fā)表于 06-13 05:19

    請問stm32f103工程代碼如何在stm32f407芯片跑起來?

    如題:1、stm32f103工程代碼如何在stm32f407芯片跑起來?2、要做哪些修改?
    發(fā)表于 09-04 09:27

    如何讓的ESP32跑起來

    ESP32是了國內(nèi)樂鑫科技推出的Wifi&藍牙物聯(lián)網(wǎng)MCU,而最近項目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應(yīng)用于更多實際項目。1ESP32簡...
    發(fā)表于 07-16 06:57

    怎樣讓自己編譯的uboot跑起來

    小目標:讓自己編譯的uboot跑起來參考:wiki.friendlyarm.com/wiki/index.php/NanoPi_NEO首先熟悉一下板子和開發(fā)流程。維基主要參考《使用全志原廠BSP
    發(fā)表于 11-08 06:37

    程序能跑起來就是很好的c代碼

    程序能跑起來并不見得代碼就是很好的c代碼了,衡量代碼的好壞應(yīng)該從以下幾個方面來添加鏈接描述看:海風教育投訴1,
    發(fā)表于 11-23 08:00

    在板子系統(tǒng)跑起來后怎么查看屏幕驅(qū)動

    請問一下,在板子系統(tǒng)跑起來后怎么查看屏幕驅(qū)動
    發(fā)表于 01-10 06:24

    如何讓u-boot跑起來?

    如何讓u-boot跑起來?
    發(fā)表于 01-26 08:26

    如何讓的ESP32跑起來

    ESP32是了國內(nèi)樂鑫科技推出的Wifi&藍牙物聯(lián)網(wǎng)MCU,而最近項目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應(yīng)用于更多實際項目。1ESP32簡介ESP32
    發(fā)表于 02-10 06:25

    STM32如何區(qū)分程序跑起來用的是HSE還是HSI呢?

    方法去區(qū)別HSE和HSI的話,我的問題就來了:燒到030f4并測到晶振有起振的程序(main下死循環(huán)),燒到030rb,晶振不起振了,但是通過仿真發(fā)現(xiàn)程序還在跑的。所以問一下大家,是怎么確定HSE跑起來了?由于程序會認為改錯配置,導致跑HSI,所以問大家
    發(fā)表于 05-05 10:47

    Zynq 7015 linux跑起來之導入之BOOT.bin生成詳解

    本文主要介紹Zynq 7015 linux跑起來之導入之BOOT.bin生成,具體的跟隨小編一起來了解一下。
    的頭像 發(fā)表于 06-27 10:01 ?7473次閱讀

    FreeRTOS_003 _讓系統(tǒng)在板子跑起來

    FreeRTOS_003_讓系統(tǒng)在板子跑起來
    的頭像 發(fā)表于 03-14 11:25 ?2767次閱讀
    FreeRTOS_003 _讓系統(tǒng)在板子<b class='flag-5'>上</b><b class='flag-5'>跑起來</b>

    windows安裝ubuntu并讓pioneer1應(yīng)用程序跑起來的過程

    本文介紹在windows下安裝ubuntu并且讓pioneer1的應(yīng)用程序跑起來的全過程。雖然安裝ubuntu不是本文重點,但是還是啰嗦地一遍吧。
    的頭像 發(fā)表于 10-23 10:41 ?2355次閱讀
    windows安裝ubuntu并讓pioneer1應(yīng)用程序<b class='flag-5'>跑起來</b>的過程

    代碼是如何跑起來的?

    今天我們來思考一個簡單的問題,一個程序是如何在 Linux 執(zhí)行起來的?
    的頭像 發(fā)表于 12-08 15:50 ?886次閱讀

    知道代碼怎樣跑起來的嗎(下)

    今天我們來思考一個簡單的問題,一個程序是如何在 Linux 執(zhí)行起來的? 我們就拿全宇宙最簡單的 Hello World 程序來舉例。
    的頭像 發(fā)表于 05-05 14:36 ?464次閱讀
    <b class='flag-5'>你</b><b class='flag-5'>知道</b><b class='flag-5'>你</b><b class='flag-5'>寫</b>的<b class='flag-5'>代碼</b>是<b class='flag-5'>怎樣</b><b class='flag-5'>跑起來</b>的嗎(下)
    RM新时代网站-首页