第一个案例(通过 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
对都使用了8个字节,因此使用了
(,%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:
对于第一个示例,进行相同的更改(将input
替换为argc
并强制使用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
函数并内联了代码)。就优化而言,编译器基本上做了同样的事情。
call
。我马上会更新我的问题。 - skink