tft每日頭條

 > 生活

 > c語言詳細講解

c語言詳細講解

生活 更新时间:2024-11-28 18:01:35
引言

在學習C語言或者其他編程語言的時候,我們編寫的一個程序代碼,基本都是在屏幕上打印出 hello world ,開始步入編程世(深)界(坑)的。C 語言版本的 hello world 代碼:

#include <stdio.h> int main() { printf("hello world\n"); return 0; }

不用多說,這段程序在運行時,會在顯示終端上打印出 hello world

那麼,這段程序背後關聯的内容,你是否真正梳理明白了呢?

  • 源程序代碼是如何編譯成可執行程序的?
  • #include<stdio.h> 的作用是什麼?
  • hello world 程序是怎樣運行起來的?
  • printf 是怎樣将字符串 "hello world" 輸出到終端的?
  • hello world 程序在運行時,它在内存中是什麼樣子的?
  • 程序的執行入口為什麼是 main 函數?
  • 可執行文件的内部結構是怎麼樣的?

閑話少說,讓我們進入正題,扒一扒 hello world 背後的内幕。

注:本文是在 Ubuntu 環境下對程序的編譯和運行進行實驗,相關内容以 Linux 系統為主。

程序編譯

在 Linux 系統或者其他環境下,将源碼編程成可執行程序,很簡單。點擊編譯按鈕或者輸入編譯指令即可完成。例如,在 Linux 下,用 gcc 編譯此程序代碼,然後運行:

$ gcc hello.c -o hello $ ./hello hello world

但是,你知道編譯器幹了哪些工作嗎?編譯器将源代碼文件編程成可執行程序,經曆了四步:編譯預處理、編譯、彙編、鍊接。

c語言詳細講解(深入理解C語言的)1

編譯過程

1. 編譯預處理

編譯預處理過程主要是處理源代碼文件中,以 “#” 開頭的預編譯指令。例如,“#inlude”、“#define”等。

預處理器根據以字符 “#” 開頭的指令,修改原始的 C 程序文件,生成一個以 .i 為擴展名的程序文件。

本例中,#include<stdio.h> 命令告訴預處理器,讀取系統頭文件 stdio.h 的内容,并把它插入到源程序文本中。

在 Linux 環境下,可以通過如下指令得到預處理完成後的 .i 文件

$ gcc -E hello.c -o hello.i

這個文件内容比較長,如果有興趣的話可以自己進行實驗,查看一下。

2. 編譯

編譯的過程就是把預處理完的文件,進行一系列的詞法分析、語法分析、語義分析以及優化後,生成相應的彙編代碼文件。這個過程往往是整個程序構建的核心部分。

将 hello.i 文件翻譯成文本文件 hello.s,其内部是一個彙編語言的程序。

通過如下指令可以得到彙編文件

$ gcc -S hello.i -o hello.s

3. 彙編

彙編器将上一步生成的彙編代碼翻譯成機器可以執行的指令,把這些指令打包成可重定位目标程序,保存在目标文件 hello.o 中。

可以通過下邊的指令生成:

$ gcc -c hello.s -o hello.o

文件 hello.o 是一個二進制文件。

4. 鍊接

hello 程序調用了 printf 函數,這是 标準 C 庫中的一個函數。printf 函數存儲在一個預編譯好的目标文件 printf.o 中,鍊接器負責将這個文件以某種方式合并到 hello.o 程序中。

合并處理後,得到一個可執行目标文件 hello,這個可執行文件可以由系統加載運行。

程序運行

hello.c 程序已經被編譯可執行的目标文件 hello,且存在磁盤上。那這個程序是如何運行起來的呢?

當然,你可以說,通過如下指令可以運行程序:

$ ./hello hello world

但是,從計算機角度來說,運行這個程序需要做哪些工作呢?

當輸入 “./hello” 後,shell 開始處理這條指令。

首先,shell 加載可執行文件 hello,複制目标文件 hello 中的代碼和數據到内存中。

數據和指令加載完成後,處理器開始執行 hello 程序中 main 函數的機器指令。這些指令将 “hello world” 字符串中的字節複制到寄存器文件,再從寄存器文件中複制顯示設備上,最終在屏幕上顯示出來。

c語言詳細講解(深入理解C語言的)2

程序執行過程

其實,操作系統在加載程序後,還做了一些工作,用于準備 main 函數執行需要的環境,然後調用 main 函數。

可執行程序文件

在 Linux 下,可執行文件的存儲格式為 ELF(Executable Linkable Format)。那麼其内部結構是什麼樣的呢?

典型的 ELF 可執行文件的布局情況如下:

c語言詳細講解(深入理解C語言的)3

可執行文件布局

ELF 頭部描述了整個文件的屬性,包括,文件是否可執行、目标硬件、目标操作系統、入口點等信息。

.init 定義了一個小函數,叫做 _init,程序的初始化代碼會調用它。

.text 為已編譯程序的機器代碼。 .rodata 為隻讀數據,比如 printf 語句中格式串。.data 為已初始化的全局和靜态 C 變量。

.bss 存放未初始化的全局變量和局部靜态變量,以及所有被初始化為 0 的全局或靜态變量。不占用實際的空間,隻是一個占位符。

.symtab 是一個符号表,存放在程序中定義和引用的函數和全局變量的信息。

.debug 一個調試符号表,内部是程序定義的局部變量和類型定義,程序定義和引用的全局變量,以及原始的 C 源文件。

.line 源程序中的行号和 .text 節中機器指令之間的映射。

.strtab 一個字符串表,内容包括 .symtab 和 .debug 節中的符号表,以及節頭部中的節名字。

總體來說,将程序源碼編譯之後生成的目标文件,主要分成兩種段:程序指令和程序數據。代碼段屬于程序指令,數據段和 .bss 段屬于程序數據。

加載可執行程序

可執行程序被加載器加載到内存,即從磁盤内複制可執行文件中的代碼和數據到内存中,然後跳轉到程序的入口點來運行該程序。将程序複制到内存并運行的過程就叫做加載

在 Linux 系統中,每個程序都有一個運行時的内存映像。

c語言詳細講解(深入理解C語言的)4

程序加載後内存布局

代碼段後邊是數段,運行時,堆在數據段之後,通過調用 malloc 庫向上增長。

用戶棧總是從最大的合法用戶地址開始,向較小内存地址增長。

用戶棧以上的區域,是為内核中的代碼和數據保留的。

程序加載運行時,會創建類似上圖所示的内存映像,在程序頭部的引導下,加載器将可執行文件複制到代碼段和數據段,然後加載器跳轉到程序的入口點。

入口點的函數調用啟動函數,初始化執行環境,然後調用用戶層的 main 函數,處理 main 函數的返回值,并在需要的時候把控制權返回給内核。

main 函數為作為用戶可執行程序的入口,是由系統啟動函數内部定義的。在環境準備好後,調用 main 函數,開始執行用戶程序。

總結

沒想到,這麼簡單的程序背後,涉及到這麼多知識内容。

  • 源碼文件編譯成可執行文件具體過程。
  • 可執行目标程序加載和執行的詳細過程。
  • 可執行目标文件内部結構布局。
  • 目标文件加載到内存後的布局情況。
,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved