C語言中,main為什麼可以不是函數?
往常C中main被定義為函數,但main也可以不是函數,為什麼C中有這樣一個奇怪的規定呢?
比如
int main=0;
可以編譯成功。
在quora的某個問題中,有以下代碼
const int main[]={
2760, -388370727, -1059470882, 1224111583, -385911411, 24,
-396877718, 86, -159547034, -562953984, 1795109192, 84891708,
-44496278, -1173395796, 141756384, -396874390, 50, -520078164,
-108828933, 1711633664, 359973375, -478100600, -2012318480,
82362563, 266874884, -2046820352, 68101336, 321584,
-492044288, 1448199142, 23744614, 1214141535, 23788169,
1711607640, -1017553320};
代碼來源:
https://www.quora.com/How-can-I-print-1-to-100-in-C++-without-a-loop-goto-or-recursion
今天寫個長答案。。。。。
先看這個測試程序,跟你的一樣結果
const int rodata[]={2760, -388370727, -1059470882, 1224111583, -385911411, 24,-396877718, 86, -159547034, -562953984, 1795109192, 84891708,-44496278, -1173395796, 141756384, -396874390, 50, -520078164,-108828933, 1711633664, 359973375, -478100600, -2012318480,82362563, 266874884, -2046820352, 68101336, 321584,-492044288, 1448199142, 23744614, 1214141535, 23788169,1711607640, -1017553320};int rwdata[]={2760, -388370727, -1059470882, 1224111583, -385911411, 24,
-396877718, 86, -159547034, -562953984, 1795109192, 84891708,-44496278, -1173395796, 141756384, -396874390, 50, -520078164,-108828933, 1711633664, 359973375, -478100600, -2012318480,82362563, 266874884, -2046820352, 68101336, 321584,-492044288, 1448199142, 23744614, 1214141535, 23788169,1711607640, -1017553320};int bssdata[40];int main(){static int staticdata = 23333;
void (*b)(void) = rodata; b();}我們先來分析一下生成的程序,看symbol table 不相干的去掉,加粗的是我們聲明的變數和main,第一列為VMA(可以理解為載入地址):
# objdump -x testmain00000000004003a8 l d .init 0000000000000000 .init00000000004003d0 l d .plt 0000000000000000 .plt0000000000400400 l d .text 0000000000000000 .text00000000004005a0 l d .rodata 0000000000000000 .rodata0000000000600ff8 l d .got 0000000000000000 .got
0000000000601000 l d .got.plt 0000000000000000 .got.plt0000000000601040 l d .data 0000000000000000 .data0000000000601100 l d .bss 0000000000000000 .bss00000000006010ec l O .data 0000000000000004 staticdata.17260000000000601060 g O .data 000000000000008c rwdata0000000000601120 g O .bss 00000000000000a0 bssdata00000000004005c0 g O .rodata 000000000000008c rodata0000000000601040 g .data 0000000000000000 __data_start0000000000000000 w *UND* 0000000000000000 __gmon_start__00000000006011c0 g .bss 0000000000000000 _end
0000000000400400 g F .text 0000000000000000 _start00000000006010f0 g .bss 0000000000000000 __bss_start00000000004004f0 g F .text 0000000000000018 main00000000004003a8 g F .init 0000000000000000 _init可以看出,staticdata/rwdata是在.data段的,而rodata在.rodata段,bssdata在.bss段,這符合我們的預期。回頭來看看實際運行時的內存映射,依然去掉不相關的:(gdb) ! cat /proc/39225/maps 00400000-00401000 r-xp 00000000 08:11 17425989 /home1/wisefox/testmain00600000-00601000 r--p 00000000 08:11 17425989 /home1/wisefox/testmain00601000-00602000 rw-p 00001000 08:11 17425989 /home1/wisefox/testmain(gdb) p bssdata
$1 = (int (*)[40]) 0x601120 &如果將原有的const去掉,它將變成一個可寫變數存儲在.data段,進而被映射進到00601000-00602000 範圍,這段就沒有執行標誌了,那麼是否可執行呢?你可以自己測試一下。
再來說說原始的程序,const int main[]會使得main變成一個const變數而存儲於.rodata段,鏈接器依照符號查找,將main的地址連入更底層的入口_start。
0000000000400400 &<_start&>: 400400: 31 ed xor %ebp,%ebp 400402: 49 89 d1 mov %rdx,%r9 400405: 5e pop %rsi 400406: 48 89 e2 mov %rsp,%rdx 400409: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 40040d: 50 push %rax 40040e: 54 push %rsp40040f: 49 c7 c0 80 05 40 00 mov $0x400580,%r8
400416: 48 c7 c1 10 05 40 00 mov $0x400510,%rcx 40041d: 48 c7 c7 f0 04 40 00 mov $0x4004f0,%rdi 400424: e8 b7 ff ff ff callq 4003e0 &<__libc_start_main@plt&> 400429: f4 hlt 40042a: 66 90 xchg %ax,%ax 40042c: 0f 1f 40 00 nopl 0x0(%rax)運行時_start調用在.rodata段main符號,而.rodata進行mmap後的頁面有可執行標誌,於是程序就正常運行了。至於數值代表的彙編:。
00000000004005c0 &4005c4: d9 ee fldz
4005c6: d9 e8 fld1 4005c8: de c1 faddp %st,%st(1) 4005ca: d9 c0 fld %st(0) 4005cc: df 75 f6 fbstp -0xa(%rbp) 4005cf: 48 8d 75 ff lea -0x1(%rbp),%rsi 4005d3: e8 18 00 00 00 callq 4005f0 &4005eb: 6a 3c pushq $0x3c
4005ed: 58 pop %rax 4005ee: 0f 05 syscall 這3行調用了第0x3c(60)的syscall:#define __NR_exit 60__SYSCALL(__NR_exit, sys_exit)4005f0: 6a 0a pushq $0xa
4005f2: 59 pop %rcx 4005f3: fd std 4005f4: ac lods %ds:(%rsi),%al 4005f5: 66 0f ba e0 07 bt $0x7,%ax 4005fa: 73 08 jae 400604 &於是就是輸出了一些數字,然後調用exit退出而已。
最後增補一點,為什麼.rodata會出現在r-xp map裡面。readelf給出的結果更清晰一些,Linux讀入文件進行map的時候是按照elf的program header來進行的,比如type=load就代表這段需要進行映射,也就是實際的程序和數據,注意對照headers和segment mapping,第一個load就是mapping序號02,包括了一大票section,其中就有.rodata 和.text,它的flags是RE,就是只讀可執行。第二個load對應了mapping序號03,flags=RW,表示可讀寫:
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000021b4 0x00000000000021b4 R E 200000LOAD 0x0000000000002e10 0x0000000000602e10 0x0000000000602e10 0x00000000000002e0 0x00000000000003b0 RW 200000 DYNAMIC 0x0000000000002e28 0x0000000000602e28 0x0000000000602e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x000000000000208c 0x000000000040208c 0x000000000040208c 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000002e10 0x0000000000602e10 0x0000000000602e10 0x00000000000001f0 0x00000000000001f0 R 1Section to Segment mapping:
Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got大家一定會奇怪實際的mapping裡面,03這個部分有些是不可寫的,有些是可寫的。那是為什麼呢?我們看看strace結果。
strace ./testmainexecve("./testmain", ["./testmain"], [/* 54 vars */]) = 0brk(0) = 0x104e000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630e0000access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=159010, ...}) = 0mmap(NULL, 159010, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f94630b9000close(3) = 0open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "177ELF2113 3 &> 1 342 "..., 832) = 832fstat(3, {st_mode=S_IFREG|0755, st_size=2112384, ...}) = 0mmap(NULL, 3936832, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9462afe000mprotect(0x7f9462cb5000, 2097152, PROT_NONE) = 0mmap(0x7f9462eb5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b7000) = 0x7f9462eb5000mmap(0x7f9462ebb000, 16960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9462ebb000close(3) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630b8000mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630b6000arch_prctl(ARCH_SET_FS, 0x7f94630b6740) = 0mprotect(0x7f9462eb5000, 16384, PROT_READ) = 0mprotect(0x602000, 4096, PROT_READ) = 0mprotect(0x7f94630e1000, 4096, PROT_READ) = 0munmap(0x7f94630b9000, 159010) = 0很明顯,loader(ld-x.xx.so)是有意使用這部分空間然後加上了readonly的許可權。我們的分析就到這裡吧。頭一次寫長答案,累死寶寶了。。。按標準的說法,main不被實現為返回int的extern函數的代碼,是ill-formed所以其實沒有說「main可以不是函數」,而是說「linkage是實現定義,但是main實現為上面這種const數組的形式是ill-formed,不保證可以正確執行」
C語言不管你main是什麼,CRT反彙編裡面是直接call main的,而main這個符號會在鏈接的時候由鏈接器尋找,然後用找到的這個名字的地址替換掉CRT啟動代碼里call main的符號,所以不管main是什麼,只要call main合法就可以了。
題目的情況,main為數組,數組名(也就是首元素地址)被解釋為函數地址調用,那數組內容就被解釋為了機器碼,如果機器碼正確,那可以被執行有兩點疑問,第一,這段數組內容所處的內存應該不具備可執行許可權,真的能被執行嗎?至少在windows下,一般的內存是不具有可執行許可權的,linux不知道有沒有這樣的許可權控制
第二,這樣的做法標準有沒有規定,我覺得應該是implementation defined的實現
p.s.剛才測試了一下,linux可用,Windows(MSVC)報錯,應該就是內存沒有可執行許可權的緣故linux安全控制好弱啊,隨便一塊內存就能執行嗎,哈哈哈哈哈哈&>&>&>好吧我不懂linux,亂黑的,無視掉吧&<&<&<p.p.s.用VS看反彙編調試了一下,完全沒法執行,訪問衝突雖然沒法執行,反彙編還是能看的,用了x87浮點指令集嘛,哦還有syscall,目測就算內存有可執行許可權,也沒法執行
推薦閱讀:
※有沒有中英文均有,且有字重和斜體的等寬字體?
※當我們討論一個功能是用軟體實現還是用硬體實現時,我們究竟關注的是什麼?
※有哪些不錯的大型項目代碼瀏覽工具?
※怎樣做到C語言和Python能夠均衡的一起學習?
※程序員如何形成自己的編碼風格?