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

2020-12-27 澎湃新聞

機器之心轉載

作者:CJ Ting

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

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

// hello.c#include int 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 並沒有出錯,所以這裡是怎麼回事?按道理來說第一行是非法的 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

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

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

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

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

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

# 運行 Go 文件的指令是 `go run`,不是一個獨立的程序# 所以,我們先要寫一個腳本包裝一下$ cat /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 在腳本模式下,我們的成績是 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#include int 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 提供的所有系統調用。

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

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

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

write: 向終端列印字符實際上就是向終端對應的文件寫入數據

exit: 退出程序

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

最終代碼如下,在 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 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, 1 mov rdi, 1 mov rsi, message mov rdx, 13 syscall mov rax, 60 xor rdi, rdi syscall

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

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

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

Step7: Handmade Binary

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

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

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

BITS 64 org 0x400000

ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 2 ; e_type dw 0x3e ; e_machine dd 1 ; e_version dq _start ; e_entry dq phdr - $$ ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrndxehdrsize equ $ - ehdr

phdr: ; Elf64_Phdr dd 1 ; p_type dd 5 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr dq $$ ; p_paddr dq filesize ; p_filesz dq filesize ; p_memsz dq 0x1000 ; p_alignphdrsize equ $ - phdr

_start: mov rax, 1 mov rdi, 1 mov rsi, message mov rdx, 13 syscall mov rax, 60 xor rdi, rdi syscall

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/

本周六,WAIC開發者·2020上海臨港人工智慧開發者大會將震撼來襲。

全球頂尖AI專家齊聚主論壇,解讀智能化轉型的技術之道。4場技術工作坊分享智慧城市、智能出行、華為開源生態與邊緣智能引擎等話題,2場黑客松等你來戰。

原標題:《只有170位元組,最小的64位Hello World程序這樣寫成》

閱讀原文

相關焦點

  • 程序'猿'的「hello,world!」是什麼梗?
    一日,飯後突生雅興,一番磨墨擬紙,並點上了上好的檀香,頗有王羲之風範,又具顏真卿氣勢,定神片刻,潑墨揮毫,鄭重地寫下:hello world很多人不了解程式設計師的人都覺得毫無笑點,其實如果你知道大多程式設計師學編程時第一課的內容時就明白了。
  • 先來理解一下C語言的Helloworld程序吧
    其功能強大,內容簡單,想學習編程的小夥伴們不妨先學習一下C語言。本文將介紹一下K&R所寫的經典程序:Helloworld的簡要分析。 各位好,編程能力作為新時代的重要能力之一,現在變得越來越熱門,本人作為未來的計算人很樂意簡單介紹一下編程的一些小知識,所以往下看吧,不會後悔的。
  • hello world! 看電影《太空旅客》
  • Hello World!菜鳥之路
    Hello,world!hello,everybody.哈哈,確實精通「hello,world!」的輸出~接下來讓我們看下,各種腳本語言中hello,world的輸出吧我首先接觸的是易語言。創造易語言的初衷是進行用中文來編寫程序的實踐,方便中國人以中國人的思維編寫程序,並不用再去學習西方思維。易語言的誕生極大地降低了編程的門檻和學習的難度。從2000年以來,易語言已經發展到一定的規模,功能上、用戶數量上都十分可觀。
  • 同學你會hello world嗎?給我講清楚點
    記得很清楚第一次面試阿里巴巴的時候,面試官上來讓我寫一個hello world程序當時我真的一面黑人問號的確認了三遍,面試官依舊淡定的說 是的寫完就讓我聊hello world,一個hello world聊了一個小時
  • 現代的 「Hello, World」,可不僅僅是幾行代碼而已
    有意義的傳統「Hello,World」程序的概念可以追溯到Brian Kernighan和Dennis M. Ritchie的著作《C程式語言》(第一版),通過編寫一個小程序來確認你掌握了這門語言和環境,已做好準備嘗試更複雜的功能。
  • Hello World 合集
    @梅子Zheng:有位程式設計師第一次玩湯姆貓時,對著手機說:Hello World!@施紅亮really:程式設計師幾乎是這世界上最友好的人了,因為每當他們學習一門新語言時,他們都會向世界問好(Hello,World)。@_晴-空 :昨天晚上做夢夢見我得到了九陰真經的武功秘籍,打開第一頁一看,我X,赫然寫著口訣「Hello World!」
  • 《你的名字》翻版劇情出現,即將上映hello,world?
    在小編尋找素材時,忽然發現了一個對學過編程的小編一個吸引的話題——「hello world」。當小編打開後,看到的居然是一部動漫?想當初還在電腦上敲著代碼的時候,看著自己的頭髮逐漸的變白,然後一根一根的掉落,小編機智地就停止了寫代碼的工作,我還沒結婚啊!脫髮算怎麼回事,結果小編卻喜歡上了自媒體寫文章這個興趣愛好,但是當小編看到hello world的時候還是會情不自禁地點了進去,看看到底是個什麼鬼?
  • 12 種主流程式語言輸出「 Hello World 」,你真的都會了嗎?
    作者 | Fatos Morina譯者 | 彎月,責編 | 屠敏以下為譯文:Hello world!剛開始學編程,或者學習一門新的程式語言時,我們編寫的第一個程序往往很簡單,而且往往這個程序就是輸出一個簡單的文本。在閱讀本文時,你可能心知肚明我指的是哪種最常見的文本。沒錯,我說的就是Hello world。
  • 從「Hello world!」入門C語言!
    不管是零基礎的C語言小白,還是有一定基礎想更進一步深入學習的同學都可以關注我,我會一步一步給朋友們講解C語言,在後期也會教大家一些基於C語言的算法程序的編程以及應用的,就比如數論算法,回溯策略,動態規劃策略,貪婪策略等等,讓C語言的學習不在漫無目的,無從下手!好了,現在開始今天的正題:作為C語言的入門,第一步當然是輸出「Hello world!」啦。
  • 擁抱ARM,Windows10發布新補丁,可運行64位程序
    近日,微軟在開發者中心正式推出可以在 ARM 架構電腦上模擬 64 位 x86 程序的新版 Windows 10。使用搭載ARM架構處理器的Windows 10用戶安裝該補丁之後,可以直接在電腦運行絕大多數Windows exe應用程式,不會因為兼容性問題而無法運行。
  • DNF64位客戶端怎麼樣?64位和32位有什麼區別?地下城與勇士64位更新...
    DNF64位客戶端怎麼樣?相信對於今天更新的64位客戶端很多朋友對於今天更新的64位客戶端有著非常多的好奇,地下城與勇士此前的32位客戶端和現在的64位對比有哪些驚喜表現呢,今天小編就為大家大家帶來詳細的介紹,幫助大家有更多的了解。
  • 《hello world》先行:即使世界毀滅,我也想再見你一面
    比如說《hello world》這部作品,在很多帖子以及自媒體好友中都備受好評,但是在看完整部這作品之後,忘川還是很失望的!感覺捧得太高,看完後的反差越大!②這部作品是一部原創3D動漫電影,篇幅不長,但是結局反轉很大!正如電影官方海報上所言:故事可能會在最後一秒反轉!其次,由於是3D作品,整體看起來十分奇怪,好在製作流暢,環境優美,總體還算過得去。
  • 詳解對ELF64之手動脫殼的逆向分析
    64位的程序?readelf -a upx_linux,下圖可以看到為64位程序;上圖為ELF文件頭,前四個字節為標識:7f 45 4c 46,意思就是:ELF,下圖:第5個字節標識:為1,則32位;為2,則為64位;這裡建議用010editor這個編輯器
  • 外交部這樣回應……字節系概念股...
    繼印度封禁抖音海外版TikTok等59款中國應用後,美國也傳出消息稱,考慮禁止中國社交媒體應用程式。也不全然都是壞消息,最新行業數據顯示,抖音6月實現營收逾9070萬美元,繼續穩居全球第一。TikTok同時也是2020上半年海外最受歡迎的移動應用,其下載量排名前三的海外市場為印度,巴西和美國,分別佔27.5%,9.6%和8.2%。字節系概念集體走弱7月8日,一直表現強勢的字節跳動概念、抖音概念板塊集體走弱。
  • ...中手遊回應字節跳動收購傳聞:不屬實;可口可樂商店微信小程序上線
    按照上市流程,最快將在1月底路演,並於春節前掛牌。多位基金經理就快手公司當前的財務數據以及未來可預見的廣告增速問題預測,快手公司的估值合理區間為450億美元。但是最後的估值需要在1月底路演結束後確定。但另據媒體報導,消息稱快手赴港上市日期擬定在明年2月5日,有承銷商透露,預期目標直指500億美金估值並持續加溫。11月5日,快手正式向港交所遞交招股申請。
  • 按字節編址與按字長編址區別及原理圖解分析
    計算機或者嵌入式剛剛入門的人,可能會有這樣的疑問:CPU 是 32 位/64 位的計算機,是怎麼讀取一個字節的?又或者說,是怎麼一下子讀取到 32 位/64 位數據的?這其實就是計算機的編址的問題。只要知道了,按字節編址與按字長編址的區別就可以解決這些疑問了。下面將用按字節編址與按字編址的電路原理圖圖解來講述其中的奧秘。 首先,32 位/64 位 CPU 指的是 CPU 的數據總線的寬度,也是字長。
  • .net framework 4.0 64位下載|.net framework 4.0 64位官方下載...
    .net framework 4.0 64位是很多軟體和遊戲都需要的運行庫、支持win7 64位系統旗艦版,很多時間的表現為,點擊啟動程序exe沒有反應,這很可能就是.net framework 4.0沒有裝好了,是支持win7系列的64位可用的版本,就是64位系統下點擊exe沒有反應所需要的運行庫。