自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux 下“Hello World”的幕后發(fā)生了什么

系統(tǒng) Linux
今天我在想 —— 當(dāng)你在 Linux 上運(yùn)行一個(gè)簡(jiǎn)單的 “Hello World” Python 程序時(shí),發(fā)生了什么,來(lái)看一下吧。

print("hello world")

這就是在命令行下的情況:

$ python3 hello.py
hello world

但是在幕后,實(shí)際上有更多的事情在發(fā)生。我將描述一些發(fā)生的情況,并且(更重要的是)解釋一些你可以用來(lái)查看幕后情況的工具。我們將用 readelf、strace、ldd、debugfs、/procltracedd 和 stat。我不會(huì)討論任何只針對(duì) Python 的部分 —— 只研究一下當(dāng)你運(yùn)行任何動(dòng)態(tài)鏈接的可執(zhí)行文件時(shí)發(fā)生的事情。

0、在執(zhí)行 execve 之前

要啟動(dòng) Python 解釋器,很多步驟都需要先行完成。那么,我們究竟在運(yùn)行哪一個(gè)可執(zhí)行文件呢?它在何處呢?

1、解析 python3 hello.py

Shell 將 python3 hello.py 解析成一條命令和一組參數(shù):python3 和 ['hello.py']。

在此過程中,可能會(huì)進(jìn)行一些如全局?jǐn)U展等操作。舉例來(lái)說,如果你執(zhí)行 python3 *.py ,Shell 會(huì)將其擴(kuò)展到 python3 hello.py。

2、確認(rèn) python3 的完整路徑

現(xiàn)在,我們了解到需要執(zhí)行 python3。但是,這個(gè)二進(jìn)制文件的完整路徑是什么呢?解決辦法是使用一個(gè)名為 PATH 的特殊環(huán)境變量。

自行驗(yàn)證:在你的 Shell 中執(zhí)行 echo $PATH。對(duì)我來(lái)說,它的輸出如下:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

當(dāng)執(zhí)行一個(gè)命令時(shí),Shell 將會(huì)依序在 PATH 列表中的每個(gè)目錄里搜索匹配的文件。

對(duì)于 fish(我的 Shell),你可以在 這里 查看路徑解析的邏輯。它使用 stat 系統(tǒng)調(diào)用去檢驗(yàn)是否存在文件。

自行驗(yàn)證:執(zhí)行 strace -e stat bash,然后運(yùn)行像 python3 這樣的命令。你應(yīng)該會(huì)看到如下輸出:

stat("/usr/local/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/bin/python3", {st_mode=S_IFREG|0755, st_size=5479736, ...}) = 0

你可以觀察到,一旦在 /usr/bin/python3 找到了二進(jìn)制文件,搜索就會(huì)立即終止:它不會(huì)繼續(xù)去 /sbin 或 /bin 中查找。

對(duì) execvp 的補(bǔ)充說明

如果你想要不用自己重新實(shí)現(xiàn),而運(yùn)行和 Shell 同樣的 PATH 搜索邏輯,你可以使用 libc 函數(shù) execvp(或其它一些函數(shù)名中含有 p 的 exec* 函數(shù))。

3、stat 的背后運(yùn)作機(jī)制

你可能在思考,Julia,stat 到底做了什么?當(dāng)你的操作系統(tǒng)要打開一個(gè)文件時(shí),主要分為兩個(gè)步驟:

  1. 它將 文件名 映射到一個(gè)包含該文件元數(shù)據(jù)的 inode
  2. 它利用這個(gè) inode 來(lái)獲取文件的實(shí)際內(nèi)容

stat 系統(tǒng)調(diào)用只是返回文件的 inode 內(nèi)容 —— 它并不讀取任何的文件內(nèi)容。好處在于這樣做速度非???。接下來(lái)讓我們一起來(lái)快速了解一下 inode。(在 Dmitry Mazin 的這篇精彩文章 《磁盤就是一堆比特》中有更多的詳細(xì)內(nèi)容)

$ stat /usr/bin/python3
  File: /usr/bin/python3 -> python3.9
  Size: 9           Blocks: 0          IO Block: 4096   symbolic link
Device: fe01h/65025d    Inode: 6206        Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-08-03 14:17:28.890364214 +0000
Modify: 2021-04-05 12:00:48.000000000 +0000
Change: 2021-06-22 04:22:50.936969560 +0000
 Birth: 2021-06-22 04:22:50.924969237 +0000

自行驗(yàn)證:我們來(lái)實(shí)際查看一下硬盤上 inode 的確切位置。

首先,我們需要找出硬盤的設(shè)備名稱:

$ df
...
tmpfs             100016      604     99412   1% /run
/dev/vda1       25630792 14488736  10062712  60% /
...

看起來(lái)它是 /dev/vda1。接著,讓我們尋找 /usr/bin/python3 的 inode 在我們硬盤上的確切位置(在 debugfs 提示符下輸入 imap 命令):

$ sudo debugfs /dev/vda1
debugfs 1.46.2 (28-Feb-2021)
debugfs:  imap /usr/bin/python3
Inode 6206 is part of block group 0
    located at block 658, offset 0x0d00

我不清楚 debugfs 是如何確定文件名對(duì)應(yīng)的 inode 的位置,但我們暫時(shí)不需要深入研究這個(gè)。

現(xiàn)在,我們需要計(jì)算硬盤中 “塊 658,偏移量 0x0d00” 處是多少個(gè)字節(jié),這個(gè)大的字節(jié)數(shù)組就是你的硬盤。每個(gè)塊有 4096 個(gè)字節(jié),所以我們需要到 4096 * 658 + 0x0d00 字節(jié)。使用計(jì)算器可以得到,這個(gè)值是 2698496。

$ sudo dd if=/dev/vda1 bs=1 skip=2698496 count=256 2>/dev/null | hexdump -C
00000000  ff a1 00 00 09 00 00 00  f8 b6 cb 64 9a 65 d1 60  |...........d.e.`|
00000010  f0 fb 6a 60 00 00 00 00  00 00 01 00 00 00 00 00  |..j`............|
00000020  00 00 00 00 01 00 00 00  70 79 74 68 6f 6e 33 2e  |........python3.|
00000030  39 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |9...............|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000060  00 00 00 00 12 4a 95 8c  00 00 00 00 00 00 00 00  |.....J..........|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 2d cb 00 00  |............-...|
00000080  20 00 bd e7 60 15 64 df  00 00 00 00 d8 84 47 d4  | ...`.d.......G.|
00000090  9a 65 d1 60 54 a4 87 dc  00 00 00 00 00 00 00 00  |.e.`T...........|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

好極了!我們找到了 inode!你可以在里面看到 python3,這是一個(gè)很好的跡象。我們并不打算深入了解所有這些,但是 Linux 內(nèi)核的 ext4 inode 結(jié)構(gòu) 指出,前 16 位是 “模式”,即權(quán)限。所以現(xiàn)在我們將看一下 ffa1 如何對(duì)應(yīng)到文件權(quán)限。

  • ffa1 對(duì)應(yīng)的數(shù)字是 0xa1ff,或者 41471(因?yàn)?x86 是小端表示)
  • 41471 用八進(jìn)制表示就是 0120777
  • 這有些奇怪 - 那個(gè)文件的權(quán)限肯定可以是 777,但前三位是什么呢?我以前沒見過這些!你可以在 inode 手冊(cè)頁(yè) 中找到 012 的含義(向下滾動(dòng)到“文件類型和模式”)。這里有一個(gè)小的表格說 012 表示 “符號(hào)鏈接”。

我們查看一下這個(gè)文件,確實(shí)是一個(gè)權(quán)限為 777 的符號(hào)鏈接:

$ ls -l /usr/bin/python3
lrwxrwxrwx 1 root root 9 Apr  5  2021 /usr/bin/python3 -> python3.9

它確實(shí)是!耶,我們正確地解碼了它。

4、準(zhǔn)備復(fù)刻

我們尚未準(zhǔn)備好啟動(dòng) python3。首先,Shell 需要?jiǎng)?chuàng)建一個(gè)新的子進(jìn)程來(lái)進(jìn)行運(yùn)行。在 Unix 上,新的進(jìn)程啟動(dòng)的方式有些特殊 - 首先進(jìn)程克隆自己,然后運(yùn)行 execve,這會(huì)將克隆的進(jìn)程替換為新的進(jìn)程。

自行驗(yàn)證: 運(yùn)行 strace -e clone bash,然后運(yùn)行 python3。你應(yīng)該會(huì)看到類似下面的輸出:

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f03788f1a10) = 3708100

3708100 是新進(jìn)程的 PID,這是 Shell 進(jìn)程的子進(jìn)程。

這里有些工具可以查看進(jìn)程的相關(guān)信息:

  • pstree 會(huì)展示你的系統(tǒng)中所有進(jìn)程的樹狀圖
  • cat /proc/PID/stat 會(huì)顯示一些關(guān)于該進(jìn)程的信息。你可以在 man proc 中找到這個(gè)文件的內(nèi)容說明。例如,第四個(gè)字段是父進(jìn)程的PID。

新進(jìn)程的繼承

新的進(jìn)程(即將變?yōu)?nbsp;python3 的)從 Shell 中繼承了很多內(nèi)容。例如,它繼承了:

  1. 環(huán)境變量:你可以通過 cat /proc/PID/environ | tr '\0' '\n' 查看
  2. 標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤的文件描述符:通過 ls -l /proc/PID/fd 查看
  3. 工作目錄(也就是當(dāng)前目錄)
  4. 命名空間和控制組(如果它在一個(gè)容器內(nèi))
  5. 運(yùn)行它的用戶以及群組
  6. 還有可能是我此刻未能列舉出來(lái)的更多東西

5、Shell 調(diào)用 execve

現(xiàn)在我們準(zhǔn)備好啟動(dòng) Python 解釋器了!

自行驗(yàn)證:運(yùn)行 strace -f -e execve bash,接著運(yùn)行 python3。其中的 -f 參數(shù)非常重要,因?yàn)槲覀兿胍櫲魏慰赡墚a(chǎn)生的子進(jìn)程。你應(yīng)該可以看到如下的輸出:

[pid 3708381] execve("/usr/bin/python3", ["python3"], 0x560397748300 /* 21 vars */) = 0

第一個(gè)參數(shù)是這個(gè)二進(jìn)制文件,而第二個(gè)參數(shù)是命令行參數(shù)列表。這些命令行參數(shù)被放置在程序內(nèi)存的特定位置,以便在運(yùn)行時(shí)可以訪問。

那么,execve 內(nèi)部到底發(fā)生了什么呢?

6、獲取該二進(jìn)制文件的內(nèi)容

我們首先需要打開 python3 的二進(jìn)制文件并讀取其內(nèi)容。直到目前為止,我們只使用了 stat 系統(tǒng)調(diào)用來(lái)獲取其元數(shù)據(jù),但現(xiàn)在我們需要獲取它的內(nèi)容。

讓我們?cè)俅尾榭?nbsp;stat 的輸出:

$ stat /usr/bin/python3
  File: /usr/bin/python3 -> python3.9
  Size: 9           Blocks: 0          IO Block: 4096   symbolic link
Device: fe01h/65025d    Inode: 6206        Links: 1
...

該文件在磁盤上占用 0 個(gè)塊的空間。這是因?yàn)榉?hào)鏈接(python3.9)的內(nèi)容實(shí)際上是存儲(chǔ)在 inode 自身中:在下面顯示你可以看到(來(lái)自上述 inode 的二進(jìn)制內(nèi)容,以 hexdump 格式分為兩行輸出)。

00000020  00 00 00 00 01 00 00 00  70 79 74 68 6f 6e 33 2e  |........python3.|
00000030  39 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |9...............|

因此,我們將需要打開 /usr/bin/python3.9 。所有這些操作都在內(nèi)核內(nèi)部進(jìn)行,所以你并不會(huì)看到其他的系統(tǒng)調(diào)用。

每個(gè)文件都由硬盤上的一系列的  構(gòu)成。我知道我系統(tǒng)中的每個(gè)塊是 4096 字節(jié),所以一個(gè)文件的最小大小是 4096 字節(jié) —— 甚至如果文件只有 5 字節(jié),它在磁盤上仍然占用 4KB。

自行驗(yàn)證:我們可以通過 debugfs 找到塊號(hào),如下所示:(再次說明,我從 Dmitry Mazin 的《磁盤就是一堆比特》文章中得知這些步驟)。

$ debugfs /dev/vda1
debugfs:  blocks /usr/bin/python3.9
145408 145409 145410 145411 145412 145413 145414 145415 145416 145417 145418 145419 145420 145421 145422 145423 145424 145425 145426 145427 145428 145429 145430 145431 145432 145433 145434 145435 145436 145437

接下來(lái),我們可以使用 dd 來(lái)讀取文件的第一個(gè)塊。我們將塊大小設(shè)定為 4096 字節(jié),跳過 145408 個(gè)塊,然后讀取 1 個(gè)塊。

$ dd if=/dev/vda1 bs=4096 skip=145408 count=1 2>/dev/null | hexdump -C | head
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  c0 a5 5e 00 00 00 00 00  |..>.......^.....|
00000020  40 00 00 00 00 00 00 00  b8 95 53 00 00 00 00 00  |@.........S.....|
00000030  00 00 00 00 40 00 38 00  0b 00 40 00 1e 00 1d 00  |....@.8...@.....|
00000040  06 00 00 00 04 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000050  40 00 40 00 00 00 00 00  40 00 40 00 00 00 00 00  |@.@.....@.@.....|
00000060  68 02 00 00 00 00 00 00  68 02 00 00 00 00 00 00  |h.......h.......|
00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
00000080  a8 02 00 00 00 00 00 00  a8 02 40 00 00 00 00 00  |..........@.....|
00000090  a8 02 40 00 00 00 00 00  1c 00 00 00 00 00 00 00  |..@.............|

你會(huì)發(fā)現(xiàn),這樣我們得到的輸出結(jié)果與直接使用 cat 讀取文件所獲得的結(jié)果完全一致。

$ cat /usr/bin/python3.9 | hexdump -C | head
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  c0 a5 5e 00 00 00 00 00  |..>.......^.....|
00000020  40 00 00 00 00 00 00 00  b8 95 53 00 00 00 00 00  |@.........S.....|
00000030  00 00 00 00 40 00 38 00  0b 00 40 00 1e 00 1d 00  |....@.8...@.....|
00000040  06 00 00 00 04 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000050  40 00 40 00 00 00 00 00  40 00 40 00 00 00 00 00  |@.@.....@.@.....|
00000060  68 02 00 00 00 00 00 00  68 02 00 00 00 00 00 00  |h.......h.......|
00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
00000080  a8 02 00 00 00 00 00 00  a8 02 40 00 00 00 00 00  |..........@.....|
00000090  a8 02 40 00 00 00 00 00  1c 00 00 00 00 00 00 00  |..@.............|

關(guān)于魔術(shù)數(shù)字的額外說明

這個(gè)文件以 ELF 開頭,這是一個(gè)被稱為“魔術(shù)數(shù)字magic number”的標(biāo)識(shí)符,它是一種字節(jié)序列,告訴我們這是一個(gè) ELF 文件。在 Linux 上,ELF 是二進(jìn)制文件的格式。

不同的文件格式有不同的魔術(shù)數(shù)字。例如,gzip 的魔數(shù)是 1f8b。文件開頭的魔術(shù)數(shù)字就是 file blah.gz 如何識(shí)別出它是一個(gè) gzip 文件的方式。

我認(rèn)為 file 命令使用了各種啟發(fā)式方法來(lái)確定文件的類型,而其中,魔術(shù)數(shù)字是一個(gè)重要的特征。

7、尋找解釋器

我們來(lái)解析這個(gè) ELF 文件,看看里面都有什么內(nèi)容。

自行驗(yàn)證:運(yùn)行 readelf -a /usr/bin/python3.9。我得到的結(jié)果是這樣的(但是我刪減了大量的內(nèi)容):

$ readelf -a /usr/bin/python3.9
ELF Header:
    Class:                             ELF64
    Machine:                           Advanced Micro Devices X86-64
...
->  Entry point address:               0x5ea5c0
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
  INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
->      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
        ...
->        1238: 00000000005ea5c0    43 FUNC    GLOBAL DEFAULT   13 _start

從這段內(nèi)容中,我理解到:

  1. 請(qǐng)求內(nèi)核運(yùn)行 /lib64/ld-linux-x86-64.so.2 來(lái)啟動(dòng)這個(gè)程序。這就是所謂的動(dòng)態(tài)鏈接器,我們將在隨后的部分對(duì)其進(jìn)行討論。
  2. 該程序制定了一個(gè)入口點(diǎn)(位于 0x5ea5c0),那里是這個(gè)程序代碼開始的地方。

接下來(lái),讓我們一起來(lái)聊聊動(dòng)態(tài)鏈接器。

8、動(dòng)態(tài)鏈接

好的!我們已從磁盤讀取了字節(jié)數(shù)據(jù),并啟動(dòng)了這個(gè)“解釋器”。那么,接下來(lái)會(huì)發(fā)生什么呢?如果你執(zhí)行 strace -o out.strace python3,你會(huì)在 execve 系統(tǒng)調(diào)用之后觀察到一系列的信息:

execve("/usr/bin/python3", ["python3"], 0x560af13472f0 /* 21 vars */) = 0
brk(NULL)                       = 0xfcc000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=32091, ...}) = 0
mmap(NULL, 32091, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f718a1e3000
close(3)                        = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 l\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=149520, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f718a1e1000
...
close(3)                        = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3

這些內(nèi)容初看可能讓人望而生畏,但我希望你能重點(diǎn)關(guān)注這一部分:openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0" ...。這里正在打開一個(gè)被稱為 pthread 的 C 語(yǔ)言線程庫(kù),運(yùn)行 Python 解釋器時(shí)需要這個(gè)庫(kù)。

自行驗(yàn)證:如果你想知道一個(gè)二進(jìn)制文件在運(yùn)行時(shí)需要加載哪些庫(kù),你可以使用 ldd 命令。下面展示的是我運(yùn)行后的效果:

$ ldd /usr/bin/python3.9
    linux-vdso.so.1 (0x00007ffc2aad7000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2fd6554000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2fd654e000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f2fd6549000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2fd6405000)
    libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f2fd63d6000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f2fd63b9000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2fd61e3000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2fd6580000)

你可以看到,第一個(gè)列出的庫(kù)就是 /lib/x86_64-linux-gnu/libpthread.so.0,這就是它被第一個(gè)加載的原因。

關(guān)于 LD_LIBRARY_PATH

說實(shí)話,我關(guān)于動(dòng)態(tài)鏈接的理解還有些模糊,以下是我所了解的一些內(nèi)容:

  • 動(dòng)態(tài)鏈接發(fā)生在用戶空間,我的系統(tǒng)上的動(dòng)態(tài)鏈接器位于 /lib64/ld-linux-x86-64.so.2. 如果你缺少動(dòng)態(tài)鏈接器,可能會(huì)遇到一些奇怪的問題,比如這種 奇怪的“文件未找到”錯(cuò)誤
  • 動(dòng)態(tài)鏈接器使用 LD_LIBRARY_PATH 環(huán)境變量來(lái)查找?guī)?/li>
  • 動(dòng)態(tài)鏈接器也會(huì)使用 LD_PRELOAD 環(huán)境變量來(lái)覆蓋你想要的任何動(dòng)態(tài)鏈接函數(shù)(你可以使用它來(lái)進(jìn)行 有趣的魔改,或者使用像 jemalloc 這樣的替代品來(lái)替換默認(rèn)內(nèi)存分配器)
  • strace 的輸出中有一些 mprotect,因?yàn)榘踩驅(qū)?kù)代碼標(biāo)記為只讀
  • 在 Mac 上,不是使用 LD_LIBRARY_PATH(Linux),而是 DYLD_LIBRARY_PATH

你可能會(huì)有疑問,如果動(dòng)態(tài)鏈接發(fā)生在用戶空間,我們?yōu)槭裁礇]有看到大量的 stat 系統(tǒng)調(diào)用在 LD_LIBRARY_PATH 中搜索這些庫(kù),就像 Bash 在 PATH 中搜索那樣?

這是因?yàn)?nbsp;ld 在 /etc/ld.so.cache 中有一個(gè)緩存,因此所有之前已經(jīng)找到的庫(kù)都會(huì)被記錄在這里。你可以在 strace 的輸出中看到它正在打開緩存 - openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

在 完整的 strace 輸出 中,我仍然對(duì)動(dòng)態(tài)鏈接之后出現(xiàn)的一些系統(tǒng)調(diào)用感到困惑(什么是 prlimit64?本地環(huán)境的內(nèi)容是如何介入的?gconv-modules.cache 是什么?rt_sigaction 做了什么?arch_prctl 是什么?以及 set_tid_address 和 set_robust_list 是什么?)。盡管如此,我覺得已經(jīng)有了一個(gè)不錯(cuò)的開頭。

旁注:ldd 實(shí)際上是一個(gè)簡(jiǎn)單的 Shell 腳本!

在 Mastodon 上,有人 指出,ldd 實(shí)際上是一個(gè) Shell 腳本,它設(shè)置了 LD_TRACE_LOADED_OBJECTS=1 環(huán)境變量,然后啟動(dòng)程序。因此,你也可以通過以下方式實(shí)現(xiàn)相同的功能:

$ LD_TRACE_LOADED_OBJECTS=1 python3
    linux-vdso.so.1 (0x00007ffe13b0a000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f01a5a47000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f01a5a41000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f2fd6549000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2fd6405000)
    libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f2fd63d6000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f2fd63b9000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2fd61e3000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2fd6580000)

事實(shí)上,ld 也是一個(gè)可以直接運(yùn)行的二進(jìn)制文件,所以你也可以通過 /lib64/ld-linux-x86-64.so.2 --list /usr/bin/python3.9 來(lái)達(dá)到相同的效果。

關(guān)于 init 和 fini

讓我們來(lái)談?wù)勥@行 strace 輸出中的內(nèi)容:

set_tid_address(0x7f58880dca10) = 3709103

這似乎與線程有關(guān),我認(rèn)為這可能是因?yàn)?nbsp;pthread 庫(kù)(以及所有其他動(dòng)態(tài)加載的庫(kù))在加載時(shí)得以運(yùn)行初始化代碼。在庫(kù)加載時(shí)運(yùn)行的代碼位于 init 區(qū)域(或者也可能是 .ctors 區(qū)域)。

自行驗(yàn)證:讓我們使用 readelf 來(lái)看看這個(gè):

$ readelf -a /lib/x86_64-linux-gnu/libpthread.so.0
...
  [10] .rela.plt         RELA             00000000000051f0  000051f0
       00000000000007f8  0000000000000018  AI       4    26     8
  [11] .init             PROGBITS         0000000000006000  00006000
       000000000000000e  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000006010  00006010
       0000000000000560  0000000000000010  AX       0     0     16
...

這個(gè)庫(kù)沒有 .ctors 區(qū)域,只有一個(gè) .init。但是,.init 區(qū)域都有些什么內(nèi)容呢?我們可以使用 objdump 來(lái)反匯編這段代碼:

$ objdump -d /lib/x86_64-linux-gnu/libpthread.so.0
Disassembly of section .init:

0000000000006000 <_init>:
    6000:       48 83 ec 08             sub    $0x8,%rsp
    6004:       e8 57 08 00 00          callq  6860 <__pthread_initialize_minimal>
    6009:       48 83 c4 08             add    $0x8,%rsp
    600d:       c3

所以它在調(diào)用 __pthread_initialize_minimal。我在 glibc 中找到了 這個(gè)函數(shù)的代碼,盡管我不得不找到一個(gè)較早版本的 glibc,因?yàn)樵诟陌姹局?,libpthread 不再是一個(gè)獨(dú)立的庫(kù)

我不確定這個(gè) set_tid_address 系統(tǒng)調(diào)用是否實(shí)際上來(lái)自 __pthread_initialize_minimal,但至少我們知道了庫(kù)可以通過 .init 區(qū)域在啟動(dòng)時(shí)運(yùn)行代碼。

這里有一份關(guān)于 .init 區(qū)域的 elf 手冊(cè)的筆記:

$ man elf

.init 這個(gè)區(qū)域保存著對(duì)進(jìn)程初始化代碼有貢獻(xiàn)的可執(zhí)行指令。當(dāng)程序開始運(yùn)行時(shí),系統(tǒng)會(huì)安排在調(diào)用主程序入口點(diǎn)之前執(zhí)行該區(qū)域中的代碼。

在 ELF 文件中也有一個(gè)在結(jié)束時(shí)運(yùn)行的 .fini 區(qū)域,以及其他可以存在的區(qū)域 .ctors / .dtors(構(gòu)造器和析構(gòu)器)。

好的,關(guān)于動(dòng)態(tài)鏈接就說這么多。

9、轉(zhuǎn)到 _start

在動(dòng)態(tài)鏈接完成后,我們進(jìn)入到 Python 解釋器中的 _start。然后,它將執(zhí)行所有正常的 Python 解析器會(huì)做的事情。

我不打算深入討論這個(gè),因?yàn)槲以谶@里關(guān)心的是關(guān)于如何在 Linux 上運(yùn)行二進(jìn)制文件的一般性知識(shí),而不是特別針對(duì) Python 解釋器。

10、寫入字符串

不過,我們?nèi)匀恍枰蛴〕?“hello world”。在底層,Python 的 print 函數(shù)調(diào)用了 libc 中的某個(gè)函數(shù)。但是,它調(diào)用了哪一個(gè)呢?讓我們來(lái)找出答案!

自行驗(yàn)證:運(yùn)行 ltrace -o out python3 hello.py

$ ltrace -o out python3 hello.py
$ grep hello out
write(1, "hello world\n", 12) = 12

看起來(lái)它確實(shí)在調(diào)用 write 函數(shù)。

我必須承認(rèn),我對(duì) ltrace 總是有一些疑慮 —— 與我深信不疑的 strace 不同,我總是不完全確定 ltrace 是否準(zhǔn)確地報(bào)告了庫(kù)調(diào)用。但在這個(gè)情況下,它似乎有效。并且,如果我們查閱 cpython 的源代碼,它似乎在一些地方確實(shí)調(diào)用了 write() 函數(shù),所以我傾向于相信這個(gè)結(jié)果。

什么是 libc?

我們剛剛提到,Python 調(diào)用了 libc 中的 write 函數(shù)。那么,libc 是什么呢?它是 C 的標(biāo)準(zhǔn)庫(kù),負(fù)責(zé)許多基本操作,例如:

  • 用 malloc 分配內(nèi)存
  • 文件 I/O(打開/關(guān)閉文件)
  • 執(zhí)行程序(像我們之前提到的 execvp
  • 使用 getaddrinfo 查找 DNS 記錄
  • 使用 pthread 管理線程

在 Linux 上,程序不一定需要使用 libc(例如 Go 就廣為人知地未使用它,而是直接調(diào)用了 Linux 系統(tǒng)調(diào)用),但是我常用的大多數(shù)其他編程語(yǔ)言(如 node、Python、Ruby、Rust)都使用了 libc。我不確定 Java 是否也使用了。

你能通過在你的二進(jìn)制文件上執(zhí)行 ldd 命令,檢查你是否正在使用 libc:如果你看到了 libc.so.6 這樣的信息,那么你就在使用 libc。

為什么 libc 重要?

你也許在思考 —— 為何重要的是 Python 調(diào)用 libc 的 write 函數(shù),然后 libc 再調(diào)用 write 系統(tǒng)調(diào)用?為何我要著重提及 libc 是調(diào)用過程的一環(huán)?

我認(rèn)為,在這個(gè)案例中,這并不真的很重要(根據(jù)我所知,libc 的 write 函數(shù)與 write 系統(tǒng)調(diào)用的映射相當(dāng)直接)。

然而,存在不同的 libc 實(shí)現(xiàn),有時(shí)它們的行為會(huì)有所不同。兩個(gè)主要的實(shí)現(xiàn)是 glibc(GNU libc)和 musl libc。

例如,直到最近,musl 的 getaddrinfo,這是一篇關(guān)于這個(gè)問題引發(fā)的錯(cuò)誤的博客文章

關(guān)于 stdout 和終端的小插曲

在我們的程序中,stdout(1 文件描述符)是一個(gè)終端。你可以在終端上做一些有趣的事情!例如:

  1. 在終端中運(yùn)行 ls -l /proc/self/fd/1。我得到了 /dev/pts/2 的結(jié)果。
  2. 在另一個(gè)終端窗口中,運(yùn)行 echo hello > /dev/pts/2。
  3. 返回到原始終端窗口。你應(yīng)會(huì)看到 hello 被打印出來(lái)了!

暫時(shí)就到這兒吧!

希望通過上文,你對(duì) hello world 是如何打印出來(lái)的有了更深的了解!我暫時(shí)不再添加更多的細(xì)節(jié),因?yàn)檫@篇文章已經(jīng)足夠長(zhǎng)了,但顯然還有更多的細(xì)節(jié)可以探討,如果大家能提供更多的細(xì)節(jié),我可能會(huì)添加更多的內(nèi)容。如果你有關(guān)于我在這里沒提到的程序內(nèi)部調(diào)用過程的任何工具推薦,我會(huì)特別高興。

我很期待看到一份 Mac 版的解析

我對(duì) Mac OS 的一個(gè)懊惱是,我不知道如何在這個(gè)級(jí)別上解讀我的系統(tǒng)——當(dāng)我打印 “hello world”,我無(wú)法像在 Linux 上那樣,窺視背后的運(yùn)作機(jī)制。我很希望看到一個(gè)深度的解析。

我所知道的一些在 Mac 下的對(duì)應(yīng)工具:

  • ldd -> otool -L
  • readelf -> otool
  • 有人說你可以在 Mac 上使用 dtruss 或 dtrace 來(lái)代替 strace,但我尚未有足夠的勇氣關(guān)閉系統(tǒng)完整性保護(hù)來(lái)讓它工作。
  • strace -> sc_usage 似乎能夠收集關(guān)于系統(tǒng)調(diào)用使用情況的統(tǒng)計(jì)信息,fs_usage 則可以收集文件使用情況的信息。

延伸閱讀

一些附加的鏈接:

責(zé)任編輯:龐桂玉 來(lái)源: Linux中國(guó)
相關(guān)推薦

2010-02-07 09:00:29

AndroidLinux Kerne

2009-08-11 10:32:23

什么是Groovy

2018-01-12 15:32:55

大數(shù)據(jù)DBA數(shù)據(jù)庫(kù)管理員

2021-12-16 15:58:48

Linux內(nèi)存微軟

2021-02-25 10:02:32

開機(jī)鍵Linux內(nèi)存

2020-08-17 12:47:07

Mozilla裁員瀏覽器

2019-11-12 14:41:41

Redis程序員Linux

2010-01-07 13:27:22

Linux驅(qū)動(dòng)程序

2017-09-06 16:20:51

2019-08-26 09:35:25

命令ping抓包

2014-12-19 10:07:10

C

2017-11-23 17:45:46

Yii框架IntelYii框架深度剖析

2021-01-18 08:23:23

內(nèi)存時(shí)底層CPU

2021-04-11 10:40:16

Git軟件開發(fā)

2015-07-03 09:27:43

網(wǎng)絡(luò)閏秒

2020-09-01 11:40:01

HTTPJavaTCP

2019-09-16 17:16:29

Hadoop數(shù)據(jù)湖數(shù)據(jù)結(jié)構(gòu)

2022-06-03 08:12:52

InnoDB插入MySQL

2020-08-20 11:50:31

語(yǔ)言類型轉(zhuǎn)換代碼

2021-11-23 23:31:43

C語(yǔ)言數(shù)據(jù)類型系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)