只有170位元組,最小的64位Hello World程序這樣寫成

2021-01-10 網易

  22點24分準時推送,第一時間送達

  

編輯:技術君 | 來源:機器之心

  上一篇:

  正文

  

最簡單的 C 語言 Hello World 程序,底層到底發生了什麼?如何編寫出最小的 64 位 Hello World 程序?

  Hello World 應該是每一位程式設計師的啟蒙程序,出自於 Brian Kernighan 和 Dennis Ritchie 的一代經典著作 The C Programming Language。

  
// hello.c#includeint main() {printf("hello, world\n");return 0;}

  這段代碼我想大家應該都太熟悉了,熟悉到可以默寫出來。雖然是非常簡單的代碼,但是如果細究起來,裡面卻隱含著很多細節:

  

  #include 和 #include "stdio.h" 有什麼區別?

  

  stdio.h 文件在哪裡?裡面是什麼內容?

  

  為什麼入口是 main 函數?可以寫一個程序入口不是 main 嗎?

  

  main 的 int 返回值有什麼用?是誰在處理 main 的返回值?

  

  printf 是誰實現的?如果不用 printf 可以做到在終端中列印字符嗎?

  

  上面這些問題其實涉及到程序的編譯、連結和裝載,日常工作中也許大家並不會在意。

  現代 IDE 在方便我們開發的同時,也將很多底層的細節隱藏了起來。往往寫完代碼以後,點擊「構建」就行了,至於構建在發生什麼,具體是怎麼構建的,很多人並不關心,甚至根本不知道從原始碼到可執行程序這中間經歷了什麼。

  編譯、連結和裝載是一個巨大的話題,不是一篇博客可以覆蓋的。在這篇博客中,我想使用「文件尺寸」作為線索,來介紹從 C 原始碼到可執行程序這個過程中,所經歷的一系列過程。

  

Tip: 關於編譯、連結和裝載,這裡想推薦一本書《程式設計師的自我修養》。不得不說,這個名字起得非常不好,很有譁眾取寵的味道,但是書的內容是不錯的,值得一看。

  我們先來編譯上面的程序:

  
$ gcc hello.c -o hello$ ./hellohello, world$ ll hello-rwxr-xr-x 1 root root 16712 Nov 24 10:45 hello

Tip: 後續所有的討論都是基於 64 位 CentOS7 作業系統。

  我們會發現這個簡單的 hello 程序大小為 16K。在今天看來,16K 真的沒什麼,但是考慮到這個程序所做的事情,它真的需要 16K 嗎?

  在 C 誕生的上個世紀 70 年代,PDP-11 的內存為 144K,如果一個 hello world 就要佔 16K,那顯然是不合理的,一定有辦法可以縮減體積。

  

Tip: 說起 C 語言,我想順帶提一下 UNIX。沒有 C 就沒有 UNIX 的成功,沒有 UNIX 的成功也就沒有 C 的今天。誕生於上個世紀 70 年代的 UNIX 不得不說是一項了不起的創造。 這裡推薦兩份關於 UNIX 的資料:The UNIX Time-Sharing System 是1974 年由 Dennis Ritchie 和 Ken Thompson 聯合發表的介紹 UNIX 的論文。不要被「論文」二字所嚇到,實際上,這篇文章寫得非常通俗易懂,由 UNIX 的作者們向你娓娓道來 UNIX 的核心設計理念。 The UNIX Operating System 是一段視頻,看身著藍色時尚毛衣的 Kernighan 演示 UNIX 的特性,不得不說,Kernighan 簡直太帥了。

  接下來我們來玩一個遊戲,目標是:在 CentOS7 64 位作業系統上,編寫一個體積最小的列印 hello world 的可執行程序。

  Executable

  我們先來看「可執行程序」這個概念。

  什麼是可執行程序?按照字面意思來理解,那就是:可以執行的程序。

  ELF

  上面用 C 編寫的 hello 當然是可執行程序,毫無疑問。

  實際上,我們可以說它是真正的「可執行」程序(區別於後文的腳本),或者說「原生」程序。

  因為它裡面包含了可以直接用於 CPU 執行的機器代碼,它的執行無需藉助外部。

  hello 的存儲格式叫做 ELF,全稱為 Executable and Linkable Format,看名稱可以知道,它既可以用於存儲目標文件,又可以用於存儲可執行文件。

  ELF 本身並不難理解,/usr/include/elf.h 中含有 ELF 結構的詳細信息。難理解的是由 ELF 所掀開的底層世界,目標文件是什麼?和執行文件有什麼區別?連結在幹什麼?目標文件怎樣變成可執行文件等等等等。

  Shebang

  接下來我們來看另外一種形式的可執行程序——腳本。

  
$ cat > hello.sh <#!/bin/bashecho "hello, world"EOF$ chmod +x hello.sh$ ./helo.shhello, world

  按照定義,因為這個腳本可以直接從命令行執行,所以它是可執行程序。

  那麼 hello 和 hello.sh 的區別在哪裡?

  可以發現 hello.sh 的第一行比較奇怪,這是一個叫做 Shebang 的東西 #!/bin/bash,這個東西表明當前文件需要 /bin/bash 程序來執行。

  所以,hello 和 hello.sh 的區別就在於:一個可以直接執行不依賴於外部程序,而另一個需要依賴外部程序。

  我曾經有一個誤解,認為 Shebang 是 Shell 在處理,當 Shell 執行腳本時,發現第一行是 Shebang,然後調用相應的程序來執行該腳本。

  實際上並不是這樣,對 Shebang 的處理是內核在進行。當內核加載一個文件時,會首先讀取文件的前 128 個字節,根據這 128 個字節判斷文件的類型,然後調用相應的加載器來加載。

  比如說,內核發現當前是一個 ELF 文件(ELF 文件前四個字節為固定值,稱為魔數),那麼就調用 ELF 加載器。

  而內核發現當前文件含有 Shebang,那麼就會啟動 Shebang 指定的程序,將當前路徑作為第一個參數傳入。所以當我們執行 ./hello.sh 時,在內核中會被變為 /bin/bash ./hello.sh。

  這裡其實有一個小問題,如果要腳本可以從命令行直接執行,那麼第一行必須是 Shebang。Shebang 的形式固定為 #! 開頭,對於使用 # 字符作為注釋的語言比如 Python, Ruby, Elixir 來說,這自然不是問題。但是對於 # 字符不是注釋字符的語言來說,這一行就是一個非法語句,必然帶來解釋錯誤。

  比如 JavaScript,它就不使用 # 作為注釋,我們來寫一個帶 Shebang 的 JS 腳本看看會怎麼樣。

  
$ cat < test.js#!/usr/bin/env nodeconsole.log("hello world")EOF$ chmod +x test.js$ ./test.jshello world

  並沒有出錯,所以這裡是怎麼回事?按道理來說第一行是非法的 JS 語句,解釋器應該要報錯才對。

  如果把第一行的 Shebang 拷貝一份到第二行,會發現報了 SyntaxError,這才是符合預期的。所以必然是 Node 什麼地方對第一行的 Shebang 做了特別處理,否則不可能不報錯。

  大家可以在 Node 的代碼裡面找一找,看看在什麼地方

  答案是什麼地方都沒有,或者說在最新的 Node 中,已經沒有地方在處理 Shebang 了。

  在 Node v11 中,我們可以看到相應的代碼(https://github.com/nodejs/node/blob/v11.15.0/lib/internal/main/check_syntax.js#L50)。

  stripShebang 函數很明顯,它的作用在於啟動 JS 解釋器的時候,將第一行的 Shebang 移除掉。

  但是在 Node v12 以後,Node 更新了 JS 引擎 V8 到 7.4,V8 在這個版本中實現一個叫做 Hashbang grammar 的功能,也就是說,從此以後,V8 可以處理 Shebang 了,因此 Node 刪除了相關代碼。

  因為 Shebang 是 V8 在處理了,所以我們在瀏覽器中也可以加載帶有 Shebang 的 JS 文件,不會有任何問題~

  我們可以得出結論,支持作為腳本使用的語言,如果不使用 # 作為注釋字符,那麼必然要特別處理 Shebang,否則使用起來就太不方便了。

  /usr/bin/env

  上面的 test.js 文件中,不知道大家是否注意到,解釋器路徑寫的是 /usr/bin/env node。

  這樣的寫法如果經常寫腳本,應該不陌生,我之前一直這樣用,但是沒有仔細去想過為什麼。

  首先我們來看 /usr/bin/env 這個程序是什麼。

  根據 man env 返回的信息:env - run a program in a modified environment.

  env 的主要作用是修改程序運行的環境變量,比如說

  
$ export name=shell$ node> process.env.name'shell'$ env name=env node> process.env.name'env'

  通過 env 我們修改了 node 運行時的環境變量。但是這個功能和我們為什麼要在 Shebang 中使用 env 有什麼關係?

  在 Shebang 中使用 env 其實是因為另外一個原因,那就是 env 會在 PATH 中搜索程序並執行。

  當我們執行 env abc 時,env 會在 PATH 中搜索 abc 然後執行,就和 Shell 一樣。

  這就解釋了為什麼我們要在腳本中使用 /usr/bin/env node。對於想要給他人復用的腳本,我們並不清楚他人系統上 node 的路徑在哪裡,但是我們清楚的是,它一定在 PATH 中。

  而同時,絕大部分系統上,env 程序的位置是固定的,那就是 /usr/bin/env。所以,通過使用 /usr/bin/env node,我們可以保證不管其他用戶將 node 安裝在何處,這個腳本都可以被執行。

  binfmt_misc

  前面我們提到過,內核對於文件的加載其實是有一套「多態」機制的,即根據不同的類型來選擇不同的加載器。

  在公眾號Python人工智慧技術後臺回復「Python進階」,獲取Python進階教程。

  那麼這個過程我們可以自己定製嗎?

  當然可以,內核中有一個加載器叫做 binfmt_misc,看名字可以知道,這個加載器用於處理各種各樣非標準的其他類型。

  通過一套語法,我們可以告知 binfmt_misc 加載規則,實現自定義加載。

  比如我們可以通過 binfmt_misc 實現直接運行 Go 文件。

  
# 運行 Go 文件的指令是 `go run`,不是一個獨立的程序# 所以,我們先要寫一個腳本包裝一下$ cat < /usr/local/bin/rungo#!/bin/bashgo run $1EOF# 接下來寫入規則告訴 binfmt_misc 使用上面的程序來加載所有# 以 .go 結尾的文件$ echo ':golang:E::go::/usr/local/bin/rungo:' > /proc/sys/fs/binfmt_misc/register# 現在我們就可以直接運行 Go 文件了$ cat << EOF > test.gopackage mainimport "fmt"func main() {fmt.Println("hello, world")}EOF$ chmod +x test.go$ ./test.gohello, world

  Tiny Script

  根據上面的知識,如果我們想要編寫一個體積最小的列印 hello world 的腳本,我們要在這兩方面著手:

  

  解釋器路徑要儘量短;

  

  腳本本身用於列印的代碼要儘量短。

  

  解釋器的路徑很好處理,我們可以使用連結。

  腳本本身的代碼要短,這就很考驗知識了,我一開始想到的是 Ruby,puts "hello, world" 算是非常短的代碼了,沒有一句廢話。但是後來 Google 才發現,還有更短的,那就是 PHP

  PHP 中 列印 hello world 的代碼就是 hello, world,對的,你沒看錯,連引號都不用。

  所以,最終我們的結果如下:

  
# 假設 php 在 /usr/local/bin/php$ cd /$ ln -s /usr/local/bin/php p$ cat < final.php#!/phello, worldEOF$ chmod +x final.php$ ./final.phphello, world$ ll final.php-rwxr-xr-x 1 root root 18 Dec 2 22:32 final.php

  在腳本模式下,我們的成績是 18 個字節,使用的解釋器是 PHP。

  其實在腳本模式下編寫最小的 hello world 沒有太大意義,因為我們完全可以自己寫一個輸出 hello world 的程序作為解釋器,然後腳本裡面只要 #!/x 就行了。

  Tiny Native

  上面的腳本只是拋磚引玉,接下來我們進入正題,怎樣編寫一個體積最小的列印 hello world 的原生可執行程序?

  網上有很多關於這個話題的討論,但基本都是針對 x86 的。現如今 64 位機器早就普及了,所以我們這裡針對的是 64 位的 x64。

  

Tip: 64 位機器可以執行 32 位的程序,比如我們可以使用 gcc -m32 來編譯 32 位程序。但這只是一個後向兼容,並沒有充分利用 64 位機器的能力。

  Step0

  首先,我們使用上文提到的 hello.c 作為基準程序。

  
// hello.c#includeint main() {printf("hello, world\n");return 0;}

  gcc hello.c -o hello.out 編譯以後,它的大小是 16712 個字節。

  Step1: Strip Symbols

  第一步,也是最容易想到的一步,剔除符號表。

  符號是連結器工作的的基本元素,原始碼中的函數、變量等被編譯以後,都變成了符號。

  如果經常從事 C 開發,一定遇到過 ld: symbol not found 的錯誤,往往是忘記連結了某個庫導致的。

  使用 nm 我們可以查看一個二進位程序中含有哪些符號。

  

Tip: nm 是「窺探」二進位的一個有力工具。記得之前有一次蘋果調整了 iOS 的審核策略,不再允許使用了 UIWebView 的 App 提交。我們的 IPA 裡面不知道哪個依賴使用了 UIWebView,導致蘋果一直審核不過,每次都要二分注釋、打包、提交審核,然後等待蘋果的自動檢查郵件告知結果,非常痛苦。 後來我想到了一個辦法,就是使用 nm 查看編譯出來的可執行程序,看看裡面是否有 UIWebView 相關的 symbol,這大大簡化了調試流程,很快就定位到問題了。

  對 step0 中的 hello.out 程序使用 nm,輸出如下:

  

  可以看到有一個符號叫做 main,這個對應的就是我們的 main 函數。 但是很奇怪沒有看到 printf,而是出現了一個叫做 puts@@GLIBC_2.2.5 的符號。

  這裡其實是 GCC 做的一個優化,如果沒有使用格式字符串調用 printf,GCC 會將它換成 puts。

  這些符號都存儲在了 ELF 中,主要用於連結,對於可執行文件來說,符號並沒有什麼太大作用,所以我們首先可以通過剔除符號表來節省空間。

  有兩個方法,第一是通過 strip,第二是通過 GCC 參數。

  這裡我們使用第二個方法,gcc -s hello.c -o hello.out 得到新的不含符號表的可執行程序,它的大小是 14512 字節。

  雖然結果還是很大,但是我們省了 2K 左右,不錯,再接再厲。

  Step2: Optimization

  第二個比較容易想到的辦法就是優化,開啟優化以後編譯器會生成更加高效的指令,從而減小文件體積。

  使用 gcc -O3 編譯我們的程序,然後會發現,結果沒有任何變化。

  其實也非常合理,因為這個程序太簡單了,沒什麼好優化的。

  看來要再想想別的辦法。

  Step3: Remove Startup Files

  之前我們提到過一個問題,是誰在調用 main 函數?

  實際上我們編寫的程序都會被默認連結到 GCC 提供的 C 運行時庫,叫做 crt。

  通過 gcc --verbose 我們可以查看編譯連結的詳細日誌。

  

  可以發現我們的程序連結了 crt1.o, crti.o, crtbegin.o, crtend.o 以及 crtn.o。

  其中 crt1.o 裡面提供的 _start 函數是程序事實上的入口,這個函數負責準備 main 函數需要的參數,調用 main 函數以及處理 main 函數的返回值。

  上面這些 crt 文件統稱為 Start Files。所以,現在我們的思路是,可不可以不用這些啟動文件?

  _start 函數主要功能有兩個,第一是準備參數,我們的 main 不使用任何參數,所以這一部分可以忽略。

  第二是處理返回值,具體的處理方式是使用 main 函數的返回值調用 exit 系統調用進行退出。

  所以如果我們不使用啟動文件的話,只需要自己使用系統調用退出即可。

  因為我們現在不使用 _start 了,自然我們的主函數也沒必要一定要叫做 main,這裡我們改個名字突出一下這個事實。

  
#include #includeintnomain(){printf("hello, world\n");_exit(0);}

  unistd.h 裡面提供系統調用的相關函數,這裡我們使用的是 _exit。為什麼是 _exit 而不是 exit?可以參考這個回答「What is the difference between using _exit() & exit() in a conventional Linux fork-exec?」。

  通過 gcc -e nomain -nostartfiles 編譯我們的程序,其中 -e 指定入口,--nostartfiles 作用很明顯,告訴 GCC 不必連結啟動文件了。

  我們得到的結果是 13664 個字節,不錯,又向前邁進了一步。

  Step4: Remove Standard Library

  現在我們已經不使用啟動文件了,但是我們還在使用標準庫,printf 和 _exit 函數都是標準庫提供的。

  可不可以不使用標準庫?

  當然也可以。

  這裡就要說到系統調用,用戶程序和作業系統的交互通過一系列稱為「系統調用」的過程來完成。

  比如 syscall_64 是 64 位 Linux 的系統調用表,裡面列出了 Linux 提供的所有系統調用。

  在公眾號頂級架構師後臺回復「面試」,獲取騰訊Python面試題和答案。

  系統調用工作在最底層,通過約定的寄存器傳遞參數,然後使用一條特別的指令,比如 32 位 Linux 是 int 80h,64 位 Linux 是 syscall 進入系統調用,最後通過約定的寄存器獲取結果。

  C 標準庫裡面封裝了相關函數幫助我們進行系統調用,一般我們不用關心調用細節。

  現在如果我們不想使用標準庫,那麼就需要自己去完成系統調用,在 hello 程序中我們使用了兩個系統調用:

  

  因為要訪問寄存器,所以必須要使用內聯彙編。

  最終代碼如下,在 C 中內聯彙編的語法可以參考這篇文檔(https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html)。

  
char *str = "hello, world\n";
voidmyprint(){asm("movq $1, %%rax \n""movq $1, %%rdi \n""movq %0, %%rsi \n""movq $13, %%rdx \n""syscall \n": // no output: "r"(str): "rax", "rdi", "rsi", "rdx");}
voidmyexit(){asm("movq $60, %rax \n""xor %rdi, %rdi \n""syscall \n");}
intnomain(){myprint();myexit();}

  使用 gcc -nostdlib 編譯我們的程序,結果是 12912 字節。

  能去的我們都去掉了,為什麼還是這麼大???

  Step5: Custom Linker Script

  我們先來看上一步得到的結果。

  
$ readelf -S -W step4/hello.outSection Headers:[Nr] Name Type Address Off Size ES Flg Lk Inf Al[ 0] NULL 0000000000000000 000000 000000 00 0 0 0[ 1] .text PROGBITS 0000000000401000 001000 00006e 00 AX 0 0 16[ 2] .rodata PROGBITS 0000000000402000 002000 00000e 01 AMS 0 0 1[ 3] .eh_frame_hdr PROGBITS 0000000000402010 002010 000024 00 A 0 0 4[ 4] .eh_frame PROGBITS 0000000000402038 002038 000054 00 A 0 0 8[ 5] .data PROGBITS 0000000000404000 003000 000008 00 WA 0 0 8[ 6] .comment PROGBITS 0000000000000000 003008 000022 01 MS 0 0 1[ 7] .shstrtab STRTAB 0000000000000000 00302a 000040 00 0 0 1

  可以發現 Size 很小但是 Off 的值非常大,也就是說每個 Section 的體積很小,但是偏移量很大。

  使用 xxd 查看文件內容,會發現裡面有大量的 0。所以情況現在很明朗,有人在對齊。

  這裡其實是默認的 Linker Script 連結腳本在做對齊操作。

  控制連結器行為的腳本叫做 Linker Script,連結器內置了一個默認腳本,正常情況下我們使用默認的就好。

  我們先來看看默認的腳本是什麼內容。

  
$ ld --verboseGNU ld (GNU Binutils) 2.34.... = ALIGN(CONSTANT (MAXPAGESIZE));.... = ALIGN(CONSTANT (MAXPAGESIZE));...

  可以看到裡面有使用 ALIGN 來對齊某些 Section,使得他們的地址是 MAXPAGESIZE 的倍數,這裡 MAXPAGESIZE 是 4K。

  這就解釋了為什麼我們的程序那麼大。

  所以現在解決方案也就很清晰了,我們不使用默認的連結腳本,自行編寫一個。

  
$ cat > link.lds <ENTRY(nomain)SECTIONS{. = 0x8048000 + SIZEOF_HEADERS;
tiny : { *(.text) *(.data) *(.rodata*) }
/DISCARD/ : { *(*) }}EOF

  使用 gcc -T link.lds 編譯程序以後,我們得到了 584 字節,巨大的進步!

  Step6: Assembly

  還有什麼辦法能進一步壓縮嗎?

  上面我們是在 C 中使用內聯彙編,為什麼不直接使用彙編,完全拋棄 C?

  我們來試試看,其實上面的 C 代碼轉換成彙編非常直接。

  
section .datamessage: db "hello, world", 0xa
section .text
global nomainnomain:mov rax, 1mov rdi, 1mov rsi, messagemov rdx, 13syscallmov rax, 60xor rdi, rdisyscall

  這裡我們使用 nasm 彙編器,我喜歡它的語法~

  nasm -f elf64 彙編我們的程序,然後使用 ld 配合上面的自定義連結腳本連結以後得到可執行程序。

  最後的結果是 440 字節,離終點又進了一步了?~

  Step7: Handmade Binary

  還能再進一步嗎?還有什麼是我們沒控制的?

  所有的代碼都已經由我們精確掌控了,但是最終的 ELF 文件依舊是由工具生成的。

  所以,最後一步,我們來手動生成 ELF 文件,精確地控制可執行文件的每一個字節。

  
BITS 64org 0x400000
ehdr: ; Elf64_Ehdrdb 0x7f, "ELF", 2, 1, 1, 0 ; e_identtimes 8 db 0dw 2 ; e_typedw 0x3e ; e_machinedd 1 ; e_versiondq _start ; e_entrydq phdr - $$ ; e_phoffdq 0 ; e_shoffdd 0 ; e_flagsdw ehdrsize ; e_ehsizedw phdrsize ; e_phentsizedw 1 ; e_phnumdw 0 ; e_shentsizedw 0 ; e_shnumdw 0 ; e_shstrndxehdrsize equ $ - ehdr
phdr: ; Elf64_Phdrdd 1 ; p_typedd 5 ; p_flagsdq 0 ; p_offsetdq $$ ; p_vaddrdq $$ ; p_paddrdq filesize ; p_fileszdq filesize ; p_memszdq 0x1000 ; p_alignphdrsize equ $ - phdr
_start:mov rax, 1mov rdi, 1mov rsi, messagemov rdx, 13syscallmov rax, 60xor rdi, rdisyscall
message: db "hello, world", 0xa
filesize equ $ - $$

  還是使用 nasm,不過這一次,我們使用 nasm -f bin 直接得到二進位程序。

  最終結果是 170 個字節,這 170 字節的程序發送給任意的 x64 架構的 64 位 Linux,都可以列印出 hello world。

  結束了,塵埃落定。

  

Tip: 其實還可以繼續,還有一些技巧可以進一步減小體積,因為非常的「Hack」,這裡不打算說明了。有興趣的朋友可以參考《A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux》。

  Final Binary Anatomy

  最後我們來看一下這 170 字節中每一個字節是什麼,在做什麼,真正地做到對每一個字節都瞭然於胸。

  
# ELF Header00: 7f 45 4c 46 02 01 01 00 # e_ident08: 00 00 00 00 00 00 00 00 # reserved10: 02 00 # e_type12: 3e 00 # e_machine14: 01 00 00 00 # e_version18: 78 00 40 00 00 00 00 00 # e_entry20: 40 00 00 00 00 00 00 00 # e_phoff28: 00 00 00 00 00 00 00 00 # e_shoff30: 00 00 00 00 # e_flags34: 40 00 # e_ehsize36: 38 00 # e_phentsize38: 01 00 # e_phnum3a: 00 00 # e_shentsize3c: 00 00 # e_shnum3e: 00 00 # e_shstrndx
# Program Header40: 01 00 00 00 # p_type44: 05 00 00 00 # p_flags48: 00 00 00 00 00 00 00 00 # p_offset50: 00 00 40 00 00 00 00 00 # p_vaddr58: 00 00 40 00 00 00 00 00 # p_paddr60: aa 00 00 00 00 00 00 00 # p_filesz68: aa 00 00 00 00 00 00 00 # p_memsz70: 00 10 00 00 00 00 00 00 # p_align
# Code78: b8 01 00 00 00 # mov $0x1,%eax7d: bf 01 00 00 00 # mov $0x1,%edi82: 48 be 9d 00 40 00 00 00 00 00 # movabs $0x40009d,%rsi8c: ba 0d 00 00 00 # mov $0xd,%edx91: 0f 05 # syscall93: b8 3c 00 00 00 # mov $0x3c,%eax98: 48 31 ff # xor %rdi,%rdi9b: 0f 05 # syscall9d: 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a # "hello, world\n"

  可以發現 ELF Header 是 64 個字節,Program Header 是 56 字節,代碼 37 個字節,最後 13 個字節是 hello, world\n 這個字符串數據。

  從上面的反彙編中我們可以看出 x86-64 和 ARM 比起來一個顯著的特點就是 x86-64 是變長指令集,每條指令的長度並不相等。長一點的 movabs 是 10 個字節,而短一點的 syscall 只有 2 個字節。

  關於 x86-64,Intel 官方的手冊 Intel® 64 and IA-32 Architectures Software Developer Manuals 十分十分詳細,是每一個底層愛好者居家旅行的必備之物。

  tiny-x64-helloworld 倉庫中有上面每一步的代碼和編譯指令,供大家參考~

  最後,編譯、連結和裝載網際網路上有很多資料,這篇博客的目的並不是想要詳細地去介紹這裡面的知識,更多地是想作為一個楔子,幫助大家建立一個整體的認識,從而挑選自己感興趣的部分去深入學習,祝大家 Happy Coding~

  原文連結:https://cjting.me/2020/12/10/tiny-x64-helloworld/

  你還有什麼想要補充的嗎?

  你在看嗎?一起成長

相關焦點

  • 程序'猿'的「hello,world!」是什麼梗?
    一日,飯後突生雅興,一番磨墨擬紙,並點上了上好的檀香,頗有王羲之風範,又具顏真卿氣勢,定神片刻,潑墨揮毫,鄭重地寫下:hello world很多人不了解程式設計師的人都覺得毫無笑點,其實如果你知道大多程式設計師學編程時第一課的內容時就明白了。
  • 第一個python程序 helloworld
    目標第一個python程序 hellworld程序python2和pythn3的區別執行python的三種方法解釋器交互式集成開發環境IDE ---pycharm01.第一個python程序 helloworld1.pyhon
  • 先來理解一下C語言的Helloworld程序吧
    其功能強大,內容簡單,想學習編程的小夥伴們不妨先學習一下C語言。本文將介紹一下K&R所寫的經典程序:Helloworld的簡要分析。 各位好,編程能力作為新時代的重要能力之一,現在變得越來越熱門,本人作為未來的計算人很樂意簡單介紹一下編程的一些小知識,所以往下看吧,不會後悔的。
  • 第一個HarmonyOS「Hello World」運行及代碼解析
    位,macOS版本的DevEco Studio即將上線。根目錄下的單文件基本是一些項目的配置文件,我們開發的目錄主要在entry,其中我比較關注的是js目錄,看到pages下的index/index.css、index/index.js、index/index.js,以及page目錄同級下的app.js,是不是感覺像小程序的開發,是的,前端就是這麼玩的!
  • 4.Java基礎知識-HelloWorld
    >public class HelloWorld {public static void main(String[] args) {System.out.println("HelloWorld");}}運行代碼步驟:在命令行模式中, 切換到文件地址 輸入javac命令對原始碼進行編譯,生成字節碼文件
  • Java第一個程序HelloWorld
    第一步,在D:\下創建一個Hello.java文件編輯Hello.java文件,輸入內容為(注意:標點符號全為英語半角)解釋:public 意思是公共的,class用於定義類的關鍵字,Hello為類名(用戶自定義),main方法是程序的入號
  • 同學你會hello world嗎?給我講清楚點
    記得很清楚第一次面試阿里巴巴的時候,面試官上來讓我寫一個hello world程序當時我真的一面黑人問號的確認了三遍,面試官依舊淡定的說 是的寫完就讓我聊hello world,一個hello world聊了一個小時
  • 現代的 「Hello, World」,可不僅僅是幾行代碼而已
    要想構建這樣的程序,我需要回憶如何使用KEDIT等編輯器,學習如何使用AS/400軟體開發工具、構建測試庫、編輯實際的程序,然後再編譯並弄清楚如何運行。雖說客戶的程序很簡單,但我不想直接開始寫程序。於是,我創建了一個「Hello,World」項目。
  • 關於第一個C語言程序 Hello world!
    童鞋A:第一次上課,老師真狠,什麼都沒學,就寫了個程序,還要我們練習。童鞋C:這些名詞太奇怪了,不知道什麼意思第一次上C語言課,你被Hello world了麼?#include<stdio.h>/*編譯預處理命令*/int main()/*主函數頭*/{ printf("Hello world!
  • 《你的名字》翻版劇情出現,即將上映hello,world?
    在小編尋找素材時,忽然發現了一個對學過編程的小編一個吸引的話題——「hello world」。當小編打開後,看到的居然是一部動漫?想當初還在電腦上敲著代碼的時候,看著自己的頭髮逐漸的變白,然後一根一根的掉落,小編機智地就停止了寫代碼的工作,我還沒結婚啊!脫髮算怎麼回事,結果小編卻喜歡上了自媒體寫文章這個興趣愛好,但是當小編看到hello world的時候還是會情不自禁地點了進去,看看到底是個什麼鬼?
  • 華為物聯網作業系統LiteOS內核教程02-HelloWorld
    填寫工程設置,需要注意一下幾點: 工程名稱和目錄中不可以有中文或者空格 SDK版本選擇最新的IoT_LINK版本,當前最新1.0.0 硬體平臺選擇STM32L431RC_BearPi 示例工程選擇hello_world_demo
  • Go語言小書|小試牛刀,從hello world開始
    引言接著我們這本小冊子的內容,今天我們手動實現一個hello world輸出, 這是編程的慣例,用於測驗環境搭建是否簡單上手,或者是否準備好了基本的條件。在其他情況下,就不那麼明顯了——至少對編譯器來說是這樣。例如,函數返回的變量或其他變量和對象引用的變量的生存期可能很難確定。如果沒有垃圾收集,則在開發人員知道不需要這些變量的時候,由開發人員釋放與這些變量相關的內存。怎麼做?在C語言中,你會直接使用 free(str) 釋放變量。
  • RabbitMQ入門之Hello World
    RabbitMQ簡介   在介紹RabbitMQ之前,我們需要了解一些最基礎的概念,相信使用過或者聽說過RabbitMQ的人都不會陌生,但筆者還是不厭其煩地在這裡講述,因為筆者的理念是self contained。Queue:隊列。計算機數據結構中的一種基本類型,遵循「先入先出」(FIFO)的原則,比如我們日常生活中常見的排隊時的隊伍就是一個隊列。
  • 零基礎第一個C++程序,步驟詳盡,來試試呀
    今天嘗試第一個程序 1.hello world!毫無疑問,只要學習任何一種程式語言,第一個程序基本都是編寫一個能夠輸出「hello, world!」的程序,往往是最基本、最簡單的。因此,這個程序常常作為一個初學者接觸一門新的程式語言所寫的第一個程序,也經常用來測試開發、編譯環境是否能夠正常工作。
  • 《hello world》先行:即使世界毀滅,我也想再見你一面
    比如說《hello world》這部作品,在很多帖子以及自媒體好友中都備受好評,但是在看完整部這作品之後,忘川還是很失望的!感覺捧得太高,看完後的反差越大!②這部作品是一部原創3D動漫電影,篇幅不長,但是結局反轉很大!正如電影官方海報上所言:故事可能會在最後一秒反轉!其次,由於是3D作品,整體看起來十分奇怪,好在製作流暢,環境優美,總體還算過得去。
  • 高淇java之hello world的深化
    第一個JAVA程序的總結和提升Java對大小寫敏感,如果出現了大小寫拼寫錯誤,程序無法運行。關鍵字public被稱作訪問修飾符(access modifier),用於控制程序的其它部分對這段代碼的訪問級別。
  • 我們可以從Java「HelloWorld」中學到什麼?
    這是每個Java程式設計師都知道的程序。它很簡單,但是簡單的開始可以導致對更複雜概念的深入理解。在這篇文章中,我將探討從這個簡單的程序中學到什麼。1.為什麼一切都從一堂課開始?Java程序是從類構建的,每個方法和欄位都必須在一個類中。這是由於它具有面向對象的功能:一切都是一個對象,它是一個類的實例。
  • DNF64位客戶端怎麼樣?64位和32位有什麼區別?地下城與勇士64位更新...
    DNF64位客戶端怎麼樣?相信對於今天更新的64位客戶端很多朋友對於今天更新的64位客戶端有著非常多的好奇,地下城與勇士此前的32位客戶端和現在的64位對比有哪些驚喜表現呢,今天小編就為大家大家帶來詳細的介紹,幫助大家有更多的了解。
  • 微軟確認:Win10 ARM從11月起正式支持運行64位程序
    日前微軟宣布,自去年開始做對AMD64指令集架構(x64)的適配後,終於大功告成。簡單來說,ARM64 PC如今可以本地運行32位和64位ARM APP,藉助仿真層編譯運行32位x86程序,並且預計11月就能編譯運行64位exe程序了。