0、起因
有的时候,DFS总是比BFS受人喜爱——毕竟DFS简单粗暴多了,而且有些东西用BFS去做无从下手,DFS是看起来可行的选择……
但是有个问题,DFS默认直接写入系统栈,系统栈又够浅的,这个时候OI常规手法是写手工栈,acm党有的时候也会想起来研究一下旁门左道——开栈外挂。
Uva上的神贴(Set heap size and stack size in C++,http://acm.uva.es/board/viewtopic.php?f=14&t=16685)里面提到了一个拓栈外挂:
#include <stdio.h> #include <string.h> #include <stdlib.h> int main2() { char test[255 << 20]; memset(test, 42, sizeof(test)); printf(":)\n"); return 0; } int main() { int size = 256 << 20; // 256Mb char *p = malloc(size) + size; asm("movl %0, %%esp\n" "pushl $exit\n" // if you get a compile error here under mingw/cygwin, "jmp main2\n" // replace exit with _exit, and main2 with _main2. :: "r"(p)); }
事先声明:我汇编是为了研究这个问题现学现卖的,有什么地方讲的不对还望各位指正,谢谢!
1、理解原版的含义,以及原版现在碰到的问题
原版关键的思路是,从堆上申请一大块内存,然后把esp(一个指向系统栈的寄存器)所指向的空间改为刚刚手工申请的地盘,之后把退出函数先推入,再直接跳转到main2的函数调用里去。
这个总体思路是没错,可是在语法上,在现在的编译器下,那是要磕磕碰碰老半天了——毕竟当年是2007年4月,现在是2014年10月了,gcc3.*都变成gcc4.7.2了,当年32位还很流行,现在比赛环境、评测环境都是烂大街的64位,不少细节需要修改了。
为了详细说明情况,接下去将采用3个不同的测试环境
1、Win下的MinGW4.7.2
2、Linux(CentOS 6.5)下的gcc4.8.1
3、ZOJ评测
2、从攻坚Win 32位出发!
把上述代码按注释中的提示修改,放到MinGW去编译,结果得到编译错误:
Undefined reference to 'main2';
找不到main2?这也是醉了?!
于是轮到利用网络资源的时刻了
我也不知道为什么,我在一个讲ARM-GCC内联汇编的文章(http://www.ethernut.de/en/documents/arm-inline-asm.html)里偶然看到了一句话:
extern long Calc(void) asm ("CALCULATE");好吧,我们现学现卖,我们加上一句:
extern int main2(void) asm ("_main2");然后编译,没问题了!等一会就有个笑脸:)了!
好了,Win 32位环境下没事了。
3、转移到Linux 64位下的漫漫长征
接下去,开了虚拟机,扔到Linux的GCC手上编译一下,挂了……
提示是:
invalid instruction suffix for `mov' invalid instruction suffix for `push'
中间折腾来折腾去的过程不用多说,
但是发现pushl改成push就少了个错误,那是个好消息
后来查资料,知道push和mov最后的字母表示操作的单位是b(1字节),w(2字节一个word),l(4字节一个longword)
呃,等等,64位环境,8字节呢?
Pascal选手都知道qword吧……
那我们猜猜看最后一个字母可不可以是q(quadword)
于是pushl改成pushq,这里还是没问题!
好的,那下面对mov动刀
首先想到栈地址应该是个qword,于是应该试试看movq,可是编译失败……
然后,64位汇编应该和32位的不一样吧……继续搜64位汇编资料,发现esp在64位下被rsp替代了
于是语句修改成:
"movq %0, %%rsp\n" "pushq $exit\n" "jmp main2\n"
4、实战环境检测——zoj为例
于是,直接把之前的2014牡丹江H题代码套上拓栈语句试试看(注意拓栈的大小,毕竟有内存限制的)
结果无情的返回CE……而且CE的错误提示挺无厘头的:
(第二节新加的那一句)error: expected initializer before 'asm'
这个时候,想起来,应该去看看编译命令的
C++: g++ foo.c -o foo -ansi -fno-asm -O2 -Wall -lm --static -DONLINE_JUDGE
于是,把asm换成__asm__继续
然后接下来迎来了Non-zero Exit Code
这个太无解了(毕竟汇编完全不熟),然后灵机一动想到一个好办法:
在实际做事情的main2()函数里面,把之前我们常用的return 0;全部换成exit(0);
这下oj评测环境你总没办法了吧?终于迎来了AC……
(与这个的奋斗未完待续)
未完成测试的项目:
1、之前用了VC++拓栈外挂过的可不可以用这个拓栈挂过了呢?
2、这种拓栈挂安全性和普适性如何?
5、总结
不得不承认,作为一个课外自发研究项目,这个折腾来折腾去挺好玩的
神犇们肯定不屑一顾
也许不少小菜急急忙忙就收走了,然后“着火”时刻马上拉出来用了
说老实话啊,这种对水平提升帮助太小。这里贴出来只是作为个人总结,也供大家娱乐。
祝各位接下来水平能有实质性的突破!
最后感谢一下队友,感谢ACDream群里的各位神犇,特别感谢kuangbin、[CUGB]fz、[bupt]leo、[NENU]Lee_vincent等人的关心和和支持和指导。
附赠:完整修改版的G++可用拓栈外挂模板
1、Win 32位MinGW 4.7.2环境
#include <stdio.h> #include <string.h> #include <stdlib.h> extern int main2(void) __asm__ ("_main2"); int main2() { char test[255 << 20]; memset(test, 42, sizeof(test)); printf(":)\n"); exit(0); } int main() { int size = 256 << 20; // 256Mb char *p = (char *)malloc(size) + size; __asm__ __volatile__( "movl %0, %%esp\n" "pushl $_exit\n" "jmp _main2\n" :: "r"(p)); }
#include <stdio.h> #include <string.h> #include <stdlib.h> extern int main2(void) __asm__ ("main2"); int main2() { char test[255 << 20]; memset(test, 42, sizeof(test)); printf(":)\n"); exit(0); } int main() { int size = 256 << 20; // 256Mb char *p = (char *)malloc(size) + size; __asm__ __volatile__( "movq %0, %%rsp\n" "pushq $exit\n" "jmp main2\n" :: "r"(p)); }3、总体修改规律:
1)extern那个伪main函数这一步必不可少!不然编译器找不到的!
2)32位下请用longword和32位寄存器,64位下请用quadword和相应的64位寄存器
3)习惯性的return 0;还是换成exit(0);最保险了