前言
這個問題起源于我和同學的一次打賭纱控,在我過去的認知中文件用 fopen
打開后就一定要用 fclose
關(guān)閉期贫,否則將不能保存寫入的內(nèi)容,寫入的數(shù)據(jù)會存留在緩沖區(qū)中赞赖。但是經(jīng)過實際測驗后滚朵,不用 fclose
寫入的內(nèi)容也能夠保存... 痛失一瓶可樂...
在那時,我把它的原因歸結(jié)于是操作系統(tǒng)自己去保存的沒有再深究前域,今天看到一點別的東西辕近,突然想起來可能那時的想法是錯誤的,這可能是要歸功與編譯器匿垄,與操作系統(tǒng)無關(guān)移宅。
ps : 在這里,我只討論 linux 下 gcc 的情況
main 和 _start
可能很多人都不知道椿疗,我們的程序執(zhí)行的入口函數(shù)其實并不是 main 函數(shù)漏峰,而是從 _start 函數(shù)開始執(zhí)行的。
來我們測驗一下变丧,先寫一個簡單的程序
int main(void)
{
return 0;
}
對的就這么簡單就可以了芽狗,編譯然后用 readelf 命令查看一下程序入口地址
gcc example.c -o test.o
readelf -h test.o
我們得到以下輸出
ELF 頭:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
數(shù)據(jù): 2 補碼绢掰,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: DYN (共享目標文件)
系統(tǒng)架構(gòu): Advanced Micro Devices X86-64
版本: 0x1
入口點地址: 0x1020
程序頭起點: 64 (bytes into file)
Start of section headers: 14576 (bytes into file)
標志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
重點看程序入口地址那一行為 0x1020
我們將編譯后的可執(zhí)行文件用 objdump 反匯編看看痒蓬,為了方便我將它輸出重定向到文件里面來看
objdump -d test.o > test.s
可以看到 0x1020 這里剛好就是 .text 段的開始也是 _start 函數(shù)的入口地地址
Disassembly of section .text:
0000000000001020 <_start>:
1020: f3 0f 1e fa endbr64
1024: 31 ed xor %ebp,%ebp
1026: 49 89 d1 mov %rdx,%r9
1029: 5e pop %rsi
102a: 48 89 e2 mov %rsp,%rdx
102d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1031: 50 push %rax
1032: 54 push %rsp
1033: 4c 8d 05 66 01 00 00 lea 0x166(%rip),%r8 # 11a0 <__libc_csu_fini>
103a: 48 8d 0d ef 00 00 00 lea 0xef(%rip),%rcx # 1130 <__libc_csu_init>
1041: 48 8d 3d d1 00 00 00 lea 0xd1(%rip),%rdi # 1119 <main>
1048: ff 15 92 2f 00 00 callq *0x2f92(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>
104e: f4 hlt
104f: 90 nop
現(xiàn)在看來,_start 是入口函數(shù)已經(jīng)是毋庸置疑了滴劲,問題是我們的 main 函數(shù)去哪里了攻晒?
__libc_start_main
在上面一段匯編代碼中,我們可以明顯地看到 _start 函數(shù)調(diào)用了一個 __libc_start_main 的函數(shù)并且將 main 函數(shù)的地址存到了 rdi 寄存器中班挖,答案八九就是在這里了鲁捏,但是這個函數(shù)是動態(tài)鏈接的,我反匯編后并沒有得到它的代碼...
1041: 48 8d 3d d1 00 00 00 lea 0xd1(%rip),%rdi # 1119 <main> 0x1041 + 0xd1 剛好是 main 函數(shù)地址
1048: ff 15 92 2f 00 00 callq *0x2f92(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>
所以我又將它編譯了一下萧芙,不過這次用靜態(tài)鏈接给梅,不然看不到 __libc_start_main 的代碼假丧。
gcc example.c -o test.o -static
然后再用 objdump 反編譯一下,這次反編譯出來足足有 12 萬行的匯編...
objdump -d test.o > test.s
然后在反匯編文件里買年直接搜索 <__libc_start_main> 函數(shù)动羽,可以找到下面幾條關(guān)鍵代碼
## 具體流程是先將 rdi 寄存器中的 main 函數(shù)地址存放到 0x18(%rsp) 位置上包帚,再將地址給寄存器 rax 用 callq 調(diào)用
401f6a: 48 89 7c 24 18 mov %rdi,0x18(%rsp)
4023c9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4023ce: ff d0 callq *%rax
# 之后調(diào)用了將 main 函數(shù)的返回值給 edi 寄存器,調(diào)用 exit 函數(shù)
4023d0: 89 c7 mov %eax,%edi
4023d2: e8 29 5f 00 00 callq 408300 <exit>
可以看出 main 函數(shù)是在 __libc_start_main 函數(shù)中調(diào)用的运吓。
exit 和 _exit
可以看出編譯器在我們編譯過程中鏈接了很多其他的東西渴邦,這個和我們之前的問題有什么關(guān)系呢?之前的分析可以得到我們的代碼還鏈接了很多別的東西不僅有我們寫的拘哨,從上面的匯編代碼可以看出當我們調(diào)用完 main 函數(shù)后谋梭,__libc_start_main 函數(shù)會繼續(xù)調(diào)用 exit 函數(shù),而 exit 函數(shù)會關(guān)閉所有打開的流倦青,這將導致寫所有被緩沖的輸出瓮床,刪除用TMPFILE函數(shù)建立的所有臨時文件。至此我們前面的問題就解決了产镐,原因是調(diào)用了 exit 函數(shù)導致緩沖都輸出到文件里面了纤垂。簡而言之就是編譯器給我們的主程序加了個 exit 函數(shù)。下面是大致過程
_start:
call __libc_start_main
_call__libc_start_main:
call main
call exit
說到 exit 就說一下 _exit 吧磷账。
其實 exit 函數(shù)就是對 _exit 函數(shù)的一個封裝峭沦,不過 exit 函數(shù)在調(diào)用 _exit 函數(shù)之前會調(diào)用終止程序(終止程序可以通過 atexit 函數(shù)注冊),清除 IO 緩沖逃糟。
_exit 函數(shù)做了三件事:
- 關(guān)閉屬于該進程的所有文件描述符
- 進程的任何子進程都由進程 init 繼承
- 向進程的父節(jié)點發(fā)送 SIGCHLD 信號
如果我們將我們的程序這樣寫
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("test", "w");
fwrite("123", 3, 1, fp);
_exit(0);
}
則文件內(nèi)容不會保存吼鱼。
純凈的程序?
gcc 提供了一系列的參數(shù)供我們使用我們也可以用 nostartfiles
指定不鏈接我們之前分析的啟動例程
gcc test.c -e main -nostartfiles -o test.o
其中 -e 是用來指定程序入口的,由于我們現(xiàn)在不鏈接之前的啟動例程所以編譯器會找不到 _start 函數(shù)绰咽,我們必須自己指定一下入口菇肃。
現(xiàn)在可以用 objdump 反匯編看一下我們的程序,你可以看到尤為地簡潔取募,十分純凈
objdump -d test.o
也可以將我們程序里面主函數(shù)名字隨便換一下琐谤,換成 test_main,然后用 gcc 指定入口 test_main玩敏,這樣我們就創(chuàng)建了一個”沒有“主函數(shù)的程序但是可以運行的程序了斗忌。
gcc test.c -e test_main -nostartfiles
但是在程序運行結(jié)束時你應該會收到以下錯誤
[1] 10074 segmentation fault (core dumped) ./a.out
出現(xiàn)這個錯誤是因為我們的程序不像之前我們有啟動例程那樣會調(diào)用 exit 正常退出,你可以自己在末尾加個 exit 或者 _exit 函數(shù)旺聚。
底層一點
fopen
函數(shù)底層調(diào)用open
打開指定的文件织阳,返回一個文件描述符(就是一個int
類型的編號),分配一個FILE
結(jié)構(gòu)體砰粹,其中包含該文件的描述符唧躲、I/O緩沖區(qū)和當前讀寫位置等信息,返回這個FILE
結(jié)構(gòu)體的地址。就是因為 FILE 結(jié)構(gòu)體這個緩沖區(qū)的存在我們才需要刷緩沖才能將文件寫入弄痹,fwrite
fread
都是先看緩沖區(qū)是否滿或空才決定使用 write
或 read
的饭入。
之所以要使用緩沖區(qū),是因為每次 write
read
都是一次系統(tǒng)調(diào)用要進入內(nèi)核肛真,調(diào)用一個系統(tǒng)調(diào)用比用戶調(diào)用要慢很多圣拄,在用戶區(qū)開辟緩沖區(qū)可以有效減少系統(tǒng)調(diào)用,提升性能毁欣。
open
庇谆、read
、write
凭疮、close
也稱無緩沖 IO饭耳,如果我們之前的寫入用 write
的話,即使不用 close
或 exit
它也會寫進文件里面去执解,不用刷緩沖寞肖。有緩沖這么好,那我們什么時候要用無緩沖 IO 呢衰腌?
通常我們讀寫設(shè)備時通常是不希望有緩沖的新蟆,例如向代表網(wǎng)絡(luò)設(shè)備的文件寫數(shù)據(jù)就是希望數(shù)據(jù)通過網(wǎng)絡(luò)設(shè)備發(fā)送出去,而不希望只寫到緩沖區(qū)里就算完事兒了右蕊,當網(wǎng)絡(luò)設(shè)備接收到數(shù)據(jù)時應用程序也希望第一時間被通知到琼稻,所以網(wǎng)絡(luò)編程通常直接調(diào)用Unbuffered I/O函數(shù)。
PS : 雖然 Unbuffered IO 函數(shù)在用戶區(qū)沒有緩沖區(qū)饶囚,但是內(nèi)核中會有 IO 緩沖區(qū)帕翻。