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));
}
可惜這個外挂有點老了(kuangbin神牡丹江被虐哭),我們需要根據現在的實際環境與時俱進,學習新姿勢,學會靈活運用拓棧挂了
事先聲明:我彙編是為了研究這個問題現學現賣的,有什麼地方講的不對還望各位指正,謝謝!
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"
編譯,pass了,運作起來得到了笑臉,yes!
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
-fno-asm?!我也是醉了……
于是,把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));
}
2、Linux 64位gcc 4.8.1環境
#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);最保險了