第一种情况(通过switch()
)为我创建了以下内容(Linux x86_64 / gcc 4.4):
400570: ff 24 c5 b8 06 40 00 jmpq *0x4006b8(,%rax,8)
[ ... ]
400580: 31 c0 xor %eax,%eax
400582: e8 e1 fe ff ff callq 400468 <printf@plt>
400587: 31 c0 xor %eax,%eax
400589: 48 83 c4 08 add $0x8,%rsp
40058d: c3 retq
40058e: bf a4 06 40 00 mov $0x4006a4,%edi
400593: eb eb jmp 400580 <main+0x30>
400595: bf a9 06 40 00 mov $0x4006a9,%edi
40059a: eb e4 jmp 400580 <main+0x30>
40059c: bf ad 06 40 00 mov $0x4006ad,%edi
4005a1: eb dd jmp 400580 <main+0x30>
4005a3: bf b1 06 40 00 mov $0x4006b1,%edi
4005a8: eb d6 jmp 400580 <main+0x30>
[ ... ]
Contents of section .rodata:
[ ... ]
4006b8 8e054000 p ... ]
请注意,.rodata
内容@4006b8
是打印的网络字节顺序(无论出于何种原因......),该值40058e
在main
上面 - arg-initializer/jmp
块开始的地方。那里的所有mov
/jmp
对都使用八个字节,因此是(,%rax,8)
间接的。因此,在这种情况下,序列为:
jmp <to location that sets arg for printf()>
...
jmp <back to common location for the printf() invocation>
...
call <printf>
...
retq
这意味着编译器实际上已经优化了调用static
站点——而是将它们全部合并到一个单一的内联printf()
调用中。这里使用的表格是jmp ...(,%rax,8)
指令,程序代码中包含的表格。
第二个(使用显式创建的表)为我执行以下操作:
0000000000400550 <print0>:
[ ... ]
0000000000400560 <print1>:
[ ... ]
0000000000400570 <print2>:
[ ... ]
0000000000400580 <print3>:
[ ... ]
0000000000400590 <print4>:
[ ... ]
00000000004005a0 <main>:
4005a0: 48 83 ec 08 sub $0x8,%rsp
4005a4: bf d4 06 40 00 mov $0x4006d4,%edi
4005a9: 31 c0 xor %eax,%eax
4005ab: 48 8d 74 24 04 lea 0x4(%rsp),%rsi
4005b0: e8 c3 fe ff ff callq 400478 <scanf@plt>
4005b5: 8b 54 24 04 mov 0x4(%rsp),%edx
4005b9: 31 c0 xor %eax,%eax
4005bb: ff 14 d5 60 0a 50 00 callq *0x500a60(,%rdx,8)
4005c2: 31 c0 xor %eax,%eax
4005c4: 48 83 c4 08 add $0x8,%rsp
4005c8: c3 retq
[ ... ]
500a60 50054000 00000000 60054000 00000000 P.@.....`.@.....
500a70 70054000 00000000 80054000 00000000 p.@.......@.....
500a80 90054000 00000000 ..@.....
再次注意,当 objdump 打印数据部分时,字节顺序颠倒了——如果你把它们转过来,你会得到函数地址print[0-4]()
。
编译器通过间接调用目标call
- 即表使用直接在call
指令中,并且表已_显式地创建为数据。
编辑:
如果您像这样更改源:
#include <stdio.h>
static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }
void main(int argc, char **argv)
{
static void (*jt[])() = { print0, print1, print2, print3, print4 };
return jt[argc]();
}
创建的程序集main()
变为:
0000000000400550 <main>:
400550: 48 63 ff movslq %edi,%rdi
400553: 31 c0 xor %eax,%eax
400555: 4c 8b 1c fd e0 09 50 mov 0x5009e0(,%rdi,8),%r11
40055c: 00
40055d: 41 ff e3 jmpq *%r11d
哪个看起来更像你想要的?
这样做的原因是您需要“无堆栈”函数才能执行此操作 - 尾递归(通过jmp
而不是从函数返回ret
)只有在您已经完成所有堆栈清理或不需要时才有可能做任何事情,因为您在堆栈上没有什么要清理的。编译器可以(但不需要)选择在最后一次函数调用之前进行清理(在这种情况下,最后一次调用可以由 进行jmp
),但这只有在您返回从该函数获得的值或“返回void
“。而且,如前所述,如果您实际使用堆栈(就像您的示例对input
变量所做的那样),则没有什么可以使编译器强制以尾递归结果的方式撤消此操作。
编辑2:
第一个示例的反汇编,具有相同的更改(argc
而不是input
强制void main
- 没有标准一致性评论,请这是一个演示),导致以下汇编:
0000000000400500 <main>:
400500: 83 ff 04 cmp $0x4,%edi
400503: 77 0b ja 400510 <main+0x10>
400505: 89 f8 mov %edi,%eax
400507: ff 24 c5 58 06 40 00 jmpq *0x400658(,%rax,8)
40050e: 66 data16
40050f: 90 nop
400510: f3 c3 repz retq
400512: bf 3c 06 40 00 mov $0x40063c,%edi
400517: 31 c0 xor %eax,%eax
400519: e9 0a ff ff ff jmpq 400428 <printf@plt>
40051e: bf 41 06 40 00 mov $0x400641,%edi
400523: 31 c0 xor %eax,%eax
400525: e9 fe fe ff ff jmpq 400428 <printf@plt>
40052a: bf 46 06 40 00 mov $0x400646,%edi
40052f: 31 c0 xor %eax,%eax
400531: e9 f2 fe ff ff jmpq 400428 <printf@plt>
400536: bf 4a 06 40 00 mov $0x40064a,%edi
40053b: 31 c0 xor %eax,%eax
40053d: e9 e6 fe ff ff jmpq 400428 <printf@plt>
400542: bf 4e 06 40 00 mov $0x40064e,%edi
400547: 31 c0 xor %eax,%eax
400549: e9 da fe ff ff jmpq 400428 <printf@plt>
40054e: 90 nop
40054f: 90 nop
这在一种方式上更糟(做两个 jmp
而不是一个),但在另一种方式上更好(因为它消除了static
函数并内联代码)。在优化方面,编译器几乎做了同样的事情。