之前碰到的兩個問題
-
在學《C語言程序設計:現代方法》第二版22.2.7文件緩沖的時候,里面講到了
setvbuf
這個函數,並說該函數的第三個參數指明了期望的緩沖策略,該參數是三個宏之一:_IOFBF(當緩沖區為空時,從流讀入數據;當緩沖區滿時,向流寫入數據)、 _IOLBF、 _IONBF.我在ubuntu 16.04 + gcc 5.4 環境下測試滿緩沖:
#include <stdio.h> #include <stdlib.h> #define SIZE_OF_BUFFER 100 int main(int argc, char const *argv[]) { char buffer[SIZE_OF_BUFFER]; if(setvbuf(stdout, buffer, _IOFBF, sizeof(buffer))) exit(-1); fflush(stdout); printf("hello"); getchar(); return 0; }
運行輸出如下:
frank@under:~/tmp$ gcc test2.c && ./a.out hello
和預想的不符合,因為這時緩沖區是我手動創建的,大小為100字節,模式為滿緩沖,這時顯然沒有滿,然而還是立即輸出了。
-
上學期和一航有一次做反彈shell的實驗,攻擊者獲得受害者的shell后,執行類似於python這樣有提示符輸出的命令會沒有提示符輸出,必須加上
-i
參數(交互模式)才能得到立即輸出的提示符和結果。另外,即使在受害者主機上寫一個有printf "hello world"的C程序,攻擊者執行以后也得不到輸出,但是如果在輸出語句后加一個fflush,攻擊者就能夠立即看到輸出了。
C語言中流的概念
一句話,流(steam)表示任意輸入源或任意輸出的目的地。
很多程序是通過一個或多個流進行讀入和輸出的。這些流可能存儲在不同的介質(如硬盤,CD,DVD,閃存等等),也可能是不存儲文件的設備(打印機,網絡套接字)。頭文件<stdio.h>中定義了處理流的函數。(注意不僅僅只有表示文件的流)
C程序中對流的操作是通過FILE *
實現的。所以說這種數據類型表示的就是一個流。一個流對象保存了和文件(也可能不是文件,但是Unix下幾乎“一切”都是文件)連接的情況以及緩沖的狀態,還有文件位置定位符的狀態。每一個流還有一個文件末位指示器和錯誤指示器,可以通過ferror和feof來監測。(參見EOF and Errors.)
注意不要試圖創建自己的FILE *
, 讓函數庫去實現。
流與文件描述符區別與聯系
一句話,流是文件描述符的抽象,一般使用文件描述符是系統層次的調用。
當向一個文件讀入或者輸出時,既可以選擇流,也可以選擇使用文件描述符。文件描述符是int
類型的,而流是用FILE *
來表示的。
文件描述符提供了一個原始、低層次的輸入輸出接口。文件描述符和流都可以表示一個連接,可以是和設備的(例如終端),或者管道,或者一個和另一個進程的套接字,或者就是一個正常的文件(normal file)。但是,如果你想要對特殊設備進行特定的操作,你必須使用文件描述符。另外,如果你的程序需要以特殊模式進行輸入輸出(例如nonblocking, polled input, 參見File Status Flags),也必須使用文件描述符。
而流提供了一個基於原始的文件描述符的高層次接口。流接口對於所有類型的文件的操作大多都是類似的,唯一的區別就是緩沖的策略(參見下面的流緩沖)。
使用流的主要優勢是操作流的函數比文件描述符多得多,而且更加強大方便。文件描述符僅僅提供了一個單一的函數用來傳輸字符塊,但是流接口提供了很多格式化的輸入輸出(例如printf和scanf)和一些字符函數以及列讀入輸出函數。
因為流是基於文件描述符的,所以實際上你可以“拆解”一個流得到對應的文件描述符然后進行低層次的操作。相反地,你也可以先用文件描述符和一個文件建立連接,然后建立一個鏈接這個文件描述符的流對象。
通常情況下,你都應該使用流來進行輸入輸出,這樣不僅方便強大,而且可以保證程序的移植性:你可以在任何一個遵守ISO C標准的機器上使用流,但是在一個非GNU機器上你可能無法使用文件描述符。
建議在看“流緩沖”概念之前看看我翻譯的三篇文章:C語言 流緩沖、標准輸入輸出 stdio 流緩沖、輸出流緩沖的意義 何時緩沖。下面這個只是這三篇文章的一個總結。
流緩沖
在系統底層調用這個層次,數據是用write+文件描述符寫入的,這種方法將數據寫入到文件描述符對應的一個字節緩沖中。大多數語言有着非常快的函數調用,在C/C++這種語言中調用一個函數可能只需要幾個cpu周期,時間開銷幾乎可以忽略不計(只有在近端的情況下才會使用inline.)。然而,一個系統調用時間開銷是非常可觀的。在Linux上的一個系統調用可能會花費幾千個cpu周期並摻雜着上下文轉化.所以系統調用比用戶空間里的函數調用花的時間多得多。
流緩沖存在的主要目的就是為用戶空間函數抵消調用系統函數的開銷。當函數做很多寫入操作時這非常重要——否則系統調用的時間會占程序運行時間的主要部分。先輸出到流緩沖中,然后以塊為單位調用系統函數輸入到對應目的地,這樣花費的時間就會減少。
另外還有一個原因,有的時候設備可能處於堵塞狀態(想象打印機的打印速度),這個時候先把要打印的字符放在緩沖區,繼續下面的任務,會節省很多時間。(突然想到了中午在食堂坐電梯。。人多的時候很類似。。先進食堂再說)
流緩沖有三個策略:
- 無緩沖 unbuffered :從一個無緩沖的流中讀寫會馬上產生效果
- 行緩沖 line buffered:當遇到一個換行符的時候字符會以塊的形式讀寫。
- 滿緩沖 fully buffered:字符會以任意大小的塊寫入讀出。(真的是直譯。。感覺和網上一些說滿的時候才讀寫的說法不一樣,說明可能是不堵塞的時候就讀寫緩沖區,最多等到緩沖區滿)
新開的流一般是滿緩沖的,只有一個例外:當流是一個可交互設備(例如終端)的時候,流將變為行緩沖。如果想了解關於如何選擇緩沖策略,參考 Controlling Buffering 。通常情況下,默認會選擇出最方便的緩沖策略。GNU libc (glibc) 使用以下的緩沖規則:
Stream | Type | Behavior |
---|---|---|
stdin | input | line-buffered |
stdout (TTY) | output | line-buffered |
stdout (not a TTY) | output | fully-buffered |
stderr | output | unbuffered |
注意:緩沖策略是寫入流/文件的充分條件,不是必要的。緩沖區存在的意義就是在使用“Stream-level I/O”時從緩沖區進行異步塊寫入/讀出,這樣可以在設備堵塞或者寫操作很多的時候加快效率。如果一次只寫入少量數據,內核一看沒有堵塞,“干脆”就把緩沖區的內容寫入了,反正放着也是放着。
回答之前兩個問題
-
由於我們只有一個寫操作,而且寫操作僅僅只有幾個字符,所以系統一看就緩沖區里僅有的幾個字符作為一個塊調用系統函數輸出到了屏幕。如果我們大量頻繁的輸出:
#include <stdio.h> #include <stdlib.h> #define SIZE_OF_BUFFER 100 int main(int argc, char const *argv[]) { char buffer[SIZE_OF_BUFFER]; if(setvbuf(stdout, buffer, _IOFBF, sizeof(buffer))) exit(-1); fflush(stdout); for (char i = 0; i < 10; ++i) { printf("hello"); } getchar(); fflush(NULL); return 0; }
編譯運行輸出:
frank@under:~/tmp$ ./a.out hello hellohellohellohellohellohellohellohellohellofrank@under:~/tmp$
可以看到,僅僅立即輸出了第一個hello,后面連續寫入到stdout流的hello都到了緩沖區里沒有立即輸出,直到后面使用
fflush
清除所有流的緩沖區,剩下的9個hello才輸出。 -
當我們通過一個反彈shell控制遠程主機時,遠程主機的程序的輸出僅僅是一個套接字,也就是說遠程運行的程序不知道它“應該”是輸出到一個可以交互的設備,所以為了提高效率,緩沖策略將會是滿緩沖——不會立即輸出。(估計python很多高層調用也用到了GNU libc庫),給python命令加上
-i
參數,就是告訴它我想以交互模式使用它,於是緩沖策略就會改變為行緩沖,很多提示符句就會立即輸出了。同樣的,我們寫的C程序也不知道輸出設備是一個可交互的,於是緩沖策略也是滿緩沖(默認大小是4096 bytes),我們加上fflush
就強制輸出了緩沖的數據,於是就能夠在攻擊主機上看到輸出了。