在Linux中,如果man -s2 open, 我们看到两种不同的函数原型声明:
$ man -s2 open NAME open, creat - open and possibly create a file or device SYNOPSIS #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); ...<snip>...
大约14年前(刚迈出大学校园没多久),第一次看到这样的声明的时候,我很纳闷,难道gcc能像g++一样支持c++的重载(overload)?当时没搞明白,反正能编译通过,就没深究(当然那时候也没能力深究)。什么是重载?所谓重载,就是函数名相同,但是参数列表不同。
那么为什么gcc支持编译同一个函数名open但是具有不同的参数列表呢?
先用一个小例子证明gcc不支持重载但是g++支持(当然也必须支持)。
o jade.c
1 int add(int a, int b) 2 { 3 return (a + b); 4 } 5 6 int add(int a, int b, int c) 7 { 8 return (a + b + c); 9 } 10 11 int main(int argc, char *argv[]) 12 { 13 int m = add(1, 2); 14 int n = add(1, 2, 3); 15 return (m + n); 16 }
o 用gcc编译看看,有报错哦
$ gcc -g -Wall -o jade jade.c jade.c:6:5: error: conflicting types for ‘add’ int add(int a, int b, int c) ^ jade.c:1:5: note: previous definition of ‘add’ was here int add(int a, int b) ^ jade.c: In function ‘main’: jade.c:13:2: error: too few arguments to function ‘add’ int m = add(1, 2); ^ jade.c:6:5: note: declared here int add(int a, int b, int c) ^
看来gcc真的不支持重载啊, 让一个c编译器去支持c++的语法,怎么可能呢?! 当然没有这种可能。
o 改用g++编译看看
$ g++ -g -Wall -o jade jade.c $ ./jade $ echo $? 9
没得错,g++能正确处理重载函数。那么回到原来的问题,为什么gcc支持编译两种不同的open()函数原型? 还是先写个demo看看。
o foo.c
1 /* 2 * Demo to dig out how these two open() as follows are compiled by gcc. 3 * o int open(const char *pathname, int flags); 4 * o int open(const char *pathname, int flags, mode_t mode); 5 */ 6 #include <sys/types.h> 7 #include <sys/stat.h> 8 #include <fcntl.h> 9 #include <unistd.h> 10 11 int 12 main(int argc, char *argv[]) 13 { 14 /* XXX: Do NOT have any error handling to make things simple */ 15 16 if (argc != 3) 17 return -1; 18 19 char *file1 = argv[1]; 20 char *file2 = argv[2]; 21 22 int fd1 = open(file1, O_RDWR|O_APPEND); 23 int fd2 = open(file2, O_RDWR|O_APPEND|O_CREAT, 0755); 24 25 write(fd1, "hello ", 6); 26 write(fd2, "world ", 6); 27 28 close(fd1); 29 close(fd2); 30 31 return 0; 32 }
o 编译并测试
$ gcc -g -Wall -o foo foo.c $ rm -f /tmp/f1 /tmp/f2 $ echo "world" > /tmp/f1 && cat /tmp/f1 world $ ls -l /tmp/f1 -rw-r--r-- 1 veli veli 6 Apr 28 15:46 /tmp/f1 $ ./foo /tmp/f1 /tmp/f2 $ ls -l /tmp/f1 /tmp/f2 -rw-r--r-- 1 veli veli 12 Apr 28 15:46 /tmp/f1 -rwxr-xr-x 1 veli veli 6 Apr 28 15:46 /tmp/f2 $ cat /tmp/f1 world hello $ cat /tmp/f2 world
神啦,gcc貌似支持重载。请注意,貌似!好了,反汇编看看。
(gdb) set disassembly-flavor intel (gdb) disas /m main Dump of assembler code for function main: 13 { 0x0804847d <+0>: push ebp 0x0804847e <+1>: mov ebp,esp 0x08048480 <+3>: and esp,0xfffffff0 0x08048483 <+6>: sub esp,0x20 14 /* XXX: Do NOT have any error handling to make things simple */ 15 16 if (argc != 3) 0x08048486 <+9>: cmp DWORD PTR [ebp+0x8],0x3 0x0804848a <+13>: je 0x8048496 <main+25> 17 return -1; 0x0804848c <+15>: mov eax,0xffffffff 0x08048491 <+20>: jmp 0x8048537 <main+186> 18 19 char *file1 = argv[1]; 0x08048496 <+25>: mov eax,DWORD PTR [ebp+0xc] 0x08048499 <+28>: mov eax,DWORD PTR [eax+0x4] 0x0804849c <+31>: mov DWORD PTR [esp+0x10],eax 20 char *file2 = argv[2]; 0x080484a3 <+38>: mov eax,DWORD PTR [eax+0x8] 0x080484a6 <+41>: mov DWORD PTR [esp+0x14],eax 21 22 int fd1 = open(file1, O_RDWR|O_APPEND); 0x080484aa <+45>: mov DWORD PTR [esp+0x4],0x402 0x080484b2 <+53>: mov eax,DWORD PTR [esp+0x10] 0x080484b6 <+57>: mov DWORD PTR [esp],eax 0x080484b9 <+60>: call 0x8048340 <open@plt> 0x080484be <+65>: mov DWORD PTR [esp+0x18],eax 23 int fd2 = open(file2, O_RDWR|O_APPEND|O_CREAT, 0755); 0x080484c2 <+69>: mov DWORD PTR [esp+0x8],0x1ed 0x080484ca <+77>: mov DWORD PTR [esp+0x4],0x442 0x080484d2 <+85>: mov eax,DWORD PTR [esp+0x14] 0x080484d6 <+89>: mov DWORD PTR [esp],eax 0x080484d9 <+92>: call 0x8048340 <open@plt> 0x080484de <+97>: mov DWORD PTR [esp+0x1c],eax ...<snip>... 30 31 return 0; 0x08048532 <+181>: mov eax,0x0 32 } 0x08048537 <+186>: leave 0x08048538 <+187>: ret End of assembler dump. (gdb)
对于open()函数,L22有两个参数, L23则有三个参数,gcc都能将其优雅地压入栈(stack)中。这里我们就可以大胆地猜测一下了,open()函数一定是支持变参的!接下来就是找证据。用gcc -E foo.c看一看,
1 $ gcc -E foo.c | egrep open 2 extern int open (const char *__file, int __oflag, ...) __attribute__ ((__nonnull__ (1))); 3 extern int openat (int __fd, const char *__file, int __oflag, ...) 4 int fd1 = open(file1, 02|02000); 5 int fd2 = open(file2, 02|02000|0100, 0755);
注意L2行,open() 果然支持变参,Bingo! 而open()的函数原型定义在fcntl.h中,
$ egrep -in " open .*nonnull.*" /usr/include/fcntl.h 146:extern int open (const char *__file, int __oflag, ...) __nonnull ((1));
相比之下,ioctl(2)一看就知道其支持变参。
$ man -s2 ioctl NAME ioctl - control device SYNOPSIS #include <sys/ioctl.h> int ioctl(int d, int request, ...); ...<snip>...
小结:
乍一看,open(2)的man page确实给了我们这样一个假象,一个c编译器gcc竟然支持c++的重载,简直太不可思议啦。然而,透过现象看本质,gcc之所以能够友好地编译两种不同的open()函数原型,是因为,gcc不支持也不可能支持c++的重载,但是open()函数原型支持变参。(Aha, 原来是open(2)的man page误导了我!) 另外,如果对ABI有所了解,那么很容易想明白,open(2)作为一种系统调用(syscall),支持变参,合情合理。