簡體   English   中英

這個沒有 libc 的 C 程序如何工作?

[英]How does this C program without libc work?

我遇到了一個沒有 libc 的最小 HTTP 服務器: https://github.com/Francesco149/nolibc-httpd

我可以看到定義了基本的字符串處理函數,導致了write系統調用:

#define fprint(fd, s) write(fd, s, strlen(s))
#define fprintn(fd, s, n) write(fd, s, n)
#define fprintl(fd, s) fprintn(fd, s, sizeof(s) - 1)
#define fprintln(fd, s) fprintl(fd, s "\n")
#define print(s) fprint(1, s)
#define printn(s, n) fprintn(1, s, n)
#define printl(s) fprintl(1, s)
#define println(s) fprintln(1, s)

基本系統調用在 C 文件中聲明:

size_t read(int fd, void *buf, size_t nbyte);
ssize_t write(int fd, const void *buf, size_t nbyte);
int open(const char *path, int flags);
int close(int fd);
int socket(int domain, int type, int protocol);
int accept(int socket, sockaddr_in_t *restrict address,
           socklen_t *restrict address_len);
int shutdown(int socket, int how);
int bind(int socket, const sockaddr_in_t *address, socklen_t address_len);
int listen(int socket, int backlog);
int setsockopt(int socket, int level, int option_name, const void *option_value,
               socklen_t option_len);
int fork();
void exit(int status);

所以我猜魔法發生在start.S中,它包含_start和一種通過創建全局標簽來編碼系統調用的特殊方式,這些標簽通過並在 r9 中累積值以節省字節:

.intel_syntax noprefix

/* functions: rdi, rsi, rdx, rcx, r8, r9 */
/*  syscalls: rdi, rsi, rdx, r10, r8, r9 */
/*                           ^^^         */
/* stack grows from a high address to a low address */

#define c(x, n) \
.global x; \
x:; \
  add r9,n

c(exit, 3)       /* 60 */
c(fork, 3)       /* 57 */
c(setsockopt, 4) /* 54 */
c(listen, 1)     /* 50 */
c(bind, 1)       /* 49 */
c(shutdown, 5)   /* 48 */
c(accept, 2)     /* 43 */
c(socket, 38)    /* 41 */
c(close, 1)      /* 03 */
c(open, 1)       /* 02 */
c(write, 1)      /* 01 */
.global read     /* 00 */
read:
  mov r10,rcx
  mov rax,r9
  xor r9,r9
  syscall
  ret

.global _start
_start:
  xor rbp,rbp
  xor r9,r9
  pop rdi     /* argc */
  mov rsi,rsp /* argv */
  call main
  call exit

這種理解正確嗎? GCC 使用start.S中定義的符號進行系統調用,然后程序在_start中啟動並從 C 文件中調用main

另外,單獨的httpd.asm自定義二進制文件是如何工作的? 只是結合 C 源並開始組裝的手動優化組裝?

(I cloned the repo and tweaked the.c and.S to compile better with clang -Oz: 992 bytes, down from the original 1208 with gcc. See the WIP-clang-tuning branch in my fork, until I get around to cleaning啟動並發送拉取請求. 使用 clang, 系統調用的內聯 asm確實節省了整體大小, 特別是一旦 main 沒有調用也沒有 rets. 如果我想在從編譯器 output 重新生成后手動打高爾夫球整個.asm ; 那里肯定是其中的一部分,可以顯着節省,例如在循環中使用lodsb 。)


在調用這些標簽中的任何一個之前,他們似乎需要r90 ,或者使用寄存器 global var 或者gcc -ffixed-r9來告訴 GCC 永遠不要干涉該寄存器 否則 GCC 會在r9中留下任何垃圾,就像其他寄存器一樣。

他們的函數是用普通原型聲明的,而不是用 6 個 args 和0虛擬參數來讓每個調用站點實際上為零r9 ,所以這不是他們的做法。


編碼系統調用的特殊方式

我不會將其描述為“編碼系統調用”。 也許“定義系統調用包裝函數”。 他們正在為每個系統調用定義自己的包裝器 function,以一種優化的方式進入底部的一個通用處理程序。 在 C 編譯器的 asm output 中,您仍然會看到call write

(對於最終的二進制文件來說,使用 inline asm 讓編譯器在正確的寄存器中使用 args 內聯syscall指令可能會更緊湊,而不是讓它看起來像一個普通的 function 來破壞所有調用破壞的寄存器。尤其是如果使用 clang -Oz編譯,它將使用 3 字節的push 2 / pop rax而不是 5 字節的mov eax, 2來設置索書號。push push imm8 / pop / syscallcall rel32的大小相同。)


是的,您可以使用.global foo / foo:在手寫 asm 中定義函數。 您可以將其視為一個大型 function ,具有用於不同系統調用的多個入口點。 在 asm 中,無論標簽如何,執行總是傳遞到下一條指令,除非您使用 jump/call/ret 指令。 CPU 不知道標簽。

所以它就像一個沒有中斷的 C switch(){}語句break; case:標簽之間,或者像 C 標簽,您可以使用goto跳轉到。 當然,除了在 asm 中,您可以在全局 scope 中執行此操作,而在 C 中,您只能在 function 中執行此操作。 在 asm 中,您可以call而不是goto ( jmp )。

    static long callnum = 0;     // r9 = 0  before a call to any of these

    ...
    socket:
       callnum += 38;
    close:
       callnum++;         // can use inc instead of add 1
    open:                 // missed optimization in their asm
       callnum++;
    write:
       callnum++;
    read:
       tmp=callnum;
       callnum=0;
       retval = syscall(tmp, args);

或者,如果您將其重鑄為尾調用鏈,我們甚至可以省略jmp foo而只是失敗:如果您有足夠聰明的編譯器,像這樣的 C 確實可以編譯為手寫 asm。 (你可以解決 arg-type

register long callnum asm("r9");     // GCC extension

long open(args...) {
   callnum++;
   return write(args...);
}
long write(args...) {
   callnum++;
   return read(args...); // tailcall
}
long read(args...){
       tmp=callnum;
       callnum=0;            // reset callnum for next call
       return syscall(tmp, args...);
}

args...是 arg 傳遞寄存器(RDI、RSI、RDX、RCX、R8),它們只是保持不變。 R9 是 x86-64 System V 的最后一個參數傳遞寄存器,但他們沒有使用任何需要 6 個參數的系統調用。 setsockopt需要 5 個參數,因此他們無法跳過mov r10, rcx 但是他們能夠將 r9 用於其他事情,而不是需要它來傳遞第 6 個參數。


有趣的是,他們如此努力地以犧牲性能為代價來節省字節,但仍然使用xor rbp,rbp而不是xor ebp,ebp 除非他們使用gcc -Wa,-Os start.S構建,否則 GAS 不會為您優化 REX 前綴。 GCC 是否優化匯編源文件?

他們可以用xchg rax, r9 (包括 REX 的 2 個字節)而不是mov rax, r9 (REX + opcode + modrm)保存另一個字節。 代碼 golf.SE 提示 x86 機器代碼

我也使用過xchg eax, r9d因為我知道 Linux 系統調用號適合 32 位,盡管它不會節省代碼大小,因為仍然需要 REX 前綴來編碼r9d寄存器號。 此外,在它們只需要加 1 的情況下, inc r9d僅為 3 個字節,而add r9d, 1為 4 個字節(REX + opcode + modrm + imm8)。 inc的 no-modrm 短格式編碼僅在 32 位模式下可用;在 64 位模式下,它被重新用作 REX 前綴。)

mov rsi,rsp也可以將一個字節保存為push rsp / pop rsi (每個 1 個字節),而不是 3 字節的 REX + mov。 這將為在call exit之前使用xchg edi, eax返回 main 的返回值騰出空間。

但是由於他們沒有使用 libc,他們可以內聯該exit ,或者將系統調用放在_start下面,這樣他們就可以陷入其中,因為exit恰好是編號最高的系統調用! 或者至少jmp exit因為他們不需要堆棧 alignment,並且jmp rel8call rel32更緊湊。


另外,單獨的 httpd.asm 自定義二進制文件是如何工作的? 只是結合 C 源並開始組裝的手動優化組裝?

不,這是完全獨立的包含 start.S 代碼( ?_017: label 處),並且可能是手動調整的編譯器 output。 也許來自鏈接可執行文件的手動調整反匯編,因此即使對於來自手寫 asm 的部分也沒有很好的 label 名稱。 (具體來說,來自Agner Fog 的objconv ,它在其 NASM 語法反匯編中使用該格式作為標簽。)

(Ruslan 還在cmp之后指出了jnz之類的東西,而不是jne對人類具有更合適的語義含義,因此另一個跡象表明它是編譯器 output,而不是手寫。)

我不知道他們如何安排讓編譯器不要碰r9 似乎只是運氣。 自述文件表明只需編譯 .c 和 .S 就可以使用它們的 GCC 版本。

至於 ELF 標頭,請參閱文件頂部的注釋,該注釋鏈接了 A Whirlwind Tutorial on Creating Seriously Teensy ELF Executables for Linux - 你可以使用nasm -fbin和 output 組裝它是一個完整的 ELF 二進制文件,跑步。 不需要鏈接+剝離,因此您可以考慮文件中的每個字節。

你對正在發生的事情非常正確。 非常有趣,我以前從未見過這樣的東西。 但基本上如你所說,每次調用 label 時,如你所說, r9不斷累加,直到達到read ,其系統調用號為 0。這就是該命令非常聰明的原因。 假設在調用read之前r9為 0(在調用正確的系統調用之前, read label 本身將r9歸零),不需要添加,因為r9已經具有所需的正確系統調用號。 write的系統調用號為1,所以只需要從0加1即可,如宏調用所示。 open的系統調用號是 2,所以首先在open label 時將其加 1,然后在write label 時再次加 1,然后在read ZD304BA20E96D5E34Z11 時將正確的系統調用號放入rax 等等。 rdirsirdx等參數寄存器也沒有被觸及,所以它基本上就像一個普通的 function 調用。

另外,單獨的 httpd.asm 自定義二進制文件是如何工作的? 只是結合 C 源並開始組裝的手動優化組裝?

我假設你在談論這個文件 不確定這里到底發生了什么,但看起來像是手動創建了一個 ELF 文件,可能是為了進一步減小大小。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM