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 testmain

00000000004003a8 l d .init 0000000000000000 .init

00000000004003d0 l d .plt 0000000000000000 .plt

0000000000400400 l d .text 0000000000000000 .text

00000000004005a0 l d .rodata 0000000000000000 .rodata

0000000000600ff8 l d .got 0000000000000000 .got

0000000000601000 l d .got.plt 0000000000000000 .got.plt

0000000000601040 l d .data 0000000000000000 .data

0000000000601100 l d .bss 0000000000000000 .bss

00000000006010ec l O .data 0000000000000004 staticdata.1726

0000000000601060 g O .data 000000000000008c rwdata

0000000000601120 g O .bss 00000000000000a0 bssdata

00000000004005c0 g O .rodata 000000000000008c rodata

0000000000601040 g .data 0000000000000000 __data_start

0000000000000000 w *UND* 0000000000000000 __gmon_start__

00000000006011c0 g .bss 0000000000000000 _end

0000000000400400 g F .text 0000000000000000 _start

00000000006010f0 g .bss 0000000000000000 __bss_start

00000000004004f0 g F .text 0000000000000018 main

00000000004003a8 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/testmain

00600000-00601000 r--p 00000000 08:11 17425989 /home1/wisefox/testmain

00601000-00602000 rw-p 00001000 08:11 17425989 /home1/wisefox/testmain

(gdb) p bssdata

$1 = (int (*)[40]) 0x601120 &

(gdb) p rwdata

$2 = (int (*)[35]) 0x601060 &

(gdb) p rodata

$3 = (const int (*)[35]) 0x4005c0 &

(gdb) p staticdata

$4 = (int *) 0x6010ec &

依照對應關係:

.rodata .text 被對應到00400000-00401000範圍, 而這個範圍是可執行的(r-xp)。所以你如果偽造了一個函數,那麼它就是可以執行的。

如果將原有的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 %rsp

40040f: 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 &:

4005c0: c8 0a 00 00 enterq $0xa,$0x0

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 &

4005d8: 6a 20 pushq $0x20

4005da: 58 pop %rax

4005db: e8 56 00 00 00 callq 400636 &

4005e0: 66 81 7d f6 00 01 cmpw $0x100,-0xa(%rbp)

4005e6: 72 de jb 4005c6 &

4005e8: 48 31 ff xor %rdi,%rdi

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 &

4005fc: 6a 2d pushq $0x2d

4005fe: 58 pop %rax

4005ff: e8 32 00 00 00 callq 400636 &

400604: ac lods %ds:(%rsi),%al

400605: 3c 00 cmp $0x0,%al

400607: e1 fb loope 400604 &

400609: 66 83 f9 00 cmp $0x0,%cx

40060d: 75 05 jne 400614 &

40060f: 66 ff c1 inc %cx

400612: 74 15 je 400629 &

400614: 88 c3 mov %al,%bl

400616: 80 e3 f0 and $0xf0,%bl

400619: 74 0e je 400629 &

40061b: 88 c3 mov %al,%bl

40061d: c0 e8 04 shr $0x4,%al

400620: 04 30 add $0x30,%al

400622: e8 0f 00 00 00 callq 400636 &

400627: 86 d8 xchg %bl,%al

400629: 24 0f and $0xf,%al

40062b: 04 30 add $0x30,%al

40062d: e8 04 00 00 00 callq 400636 &

400632: ac lods %ds:(%rsi),%al

400633: e2 e6 loop 40061b &

400635: c3 retq

400636: 51 push %rcx

400637: 56 push %rsi

400638: 66 50 push %ax

40063a: 6a 01 pushq $0x1

40063c: 5f pop %rdi

40063d: 54 push %rsp

40063e: 5e pop %rsi

40063f: 48 89 fa mov %rdi,%rdx

400642: 6a 01 pushq $0x1

400644: 58 pop %rax

400645: 0f 05 syscall

400647: 66 58 pop %ax

400649: 5e pop %rsi

40064a: 59 pop %rcx

40064b: c3 retq

這3行調用了第1個syscall 那麼是什麼呢:

#define __NR_write 1

__SYSCALL(__NR_write, sys_write)

於是就是輸出了一些數字,然後調用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 200000

LOAD 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 1

Section 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 ./testmain

execve("./testmain", ["./testmain"], [/* 54 vars */]) = 0

brk(0) = 0x104e000

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630e0000

access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)

open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=159010, ...}) = 0

mmap(NULL, 159010, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f94630b9000

close(3) = 0

open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

read(3, "177ELF21133&>1 342"..., 832) = 832

fstat(3, {st_mode=S_IFREG|0755, st_size=2112384, ...}) = 0

mmap(NULL, 3936832, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9462afe000

mprotect(0x7f9462cb5000, 2097152, PROT_NONE) = 0

mmap(0x7f9462eb5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b7000) = 0x7f9462eb5000

mmap(0x7f9462ebb000, 16960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9462ebb000

close(3) = 0

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630b8000

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94630b6000

arch_prctl(ARCH_SET_FS, 0x7f94630b6740) = 0

mprotect(0x7f9462eb5000, 16384, PROT_READ) = 0

mprotect(0x602000, 4096, PROT_READ) = 0

mprotect(0x7f94630e1000, 4096, PROT_READ) = 0

munmap(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能夠均衡的一起學習?
程序員如何形成自己的編碼風格?

TAG:程序員 | 編程 | C編程語言 |