1. 程式人生 > >有關在ACM/ICPC競賽環境下的GCC/G++的拓棧外掛的研究記錄

有關在ACM/ICPC競賽環境下的GCC/G++的拓棧外掛的研究記錄

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);最保險了