道高一尺 or 魔高一丈:浅谈交互库编写

有些时候,为了实现特殊的需求,我们可能需要编写交互库,要求选手链接。

常见的情景有:

  • 强制在线:一些题目不强制在线可能会被“乱搞”通过。例如可持久化数据结构有众所周知的离线做法。
  • 加速输入输出:一些题目为了要求严格线性可能需要 $10^8$ 以上的输入量,如此大的数据必须在内存生成并交换。
  • 限制操作:一些思维题不允许选手直接读取数据,而是要求选手做特定询问获取详细内容;或者可能限制操作次数。
  • 人机对抗:另一些思维题要求选手找到最优策略,那么可以要求选手通过接口与交互库对抗。

既然交互库要链接选手的程序,就必须做好防范措施,避免选手使用不当操作 $\textcolor{green}{\text{AC}}$。

本人才疏学浅,谈论的内容只是全局的一小部分,欢迎补充。

Hack 输出内容

如果不要求选手进行输出,很多交互库可能只是简单输出 YesNo 来告诉 Judger 结果是否正确。

0x00

一个基本的素养是将除了 main 函数和接口函数以外的函数或变量使用 static 修饰,并且不使用 #defineusing。防止出现奇怪问题或被选手猜中变量名。

0x01

这里有一个显然的漏洞。众所周知,函数 exit 可以让程序退出,不必返回主函数,选手自然可以输出 Yes 后直接退出。

int solve(...) {
  ...
  cout << "Yes" << endl;
  exit(0);
}

0x02

最简单的方式是不告诉选手交互库评测时行为。

不过,在部分题目中,这可能是无法避免的。

一个简单的做法是利用析构函数,只要定义一个全局变量,程序退出时自然会进行析构。

static struct E {
  int code = -1;
  ~E() { cout << code << endl; }
} e;

另一个高级的方法是使用 atexit 注册函数,被注册的函数会在程序正常退出时被逆序调用。

atexit 定义在 <cstdlib>

...
static int code;
int main() {
  ...
  auto p = []{ cout << code << endl; };
  atexit(p);
  ...
}

要注意的是以上两种方法只能在函数正常退出时输出;不过程序非正常退出时一定不会返回 0,会出现 $\textcolor{purple}{\text{RE}}$。

0x03

不过最近看到一个有趣的函数:

[[noreturn]] void quick_exit( int exit_code ) noexcept;

这个函数也定义在 <cstdlib>。顾名思义,这个函数可以使程序快速退出,不完全清理资源。

#include <cstdlib>
#include <iostream>
using namespace std;

static struct E { ~E() { cout << "clean" << endl; } } e;

int main() { quick_exit(0); }

尝试运行以上程序,你会发现不会输出任何东西。

0x04

不过如果你知道 quick_exit,一定会看到 at_quick_exit,后者也定义在 <cstdlib>

at_quick_exit 可以注册快速退出时执行的函数,不会在正常退出时调用。

这段程序可能更加保险。

...
static int code;
int main() {
  ...
  auto p = []{ cout << code << endl; };
  auto q = []{ cout << "at_quick_exit" << endl; };
  atexit(p);
  at_quick_exit(q);
  ...
}

注意快速退出时不一定刷新缓冲区,需要手动刷新。

0x05

至此,在输出上直接做手脚可能比较困难,但选手程序和交互库毕竟被编译到同一程序,高手可以选择直接操作内存。

参见 部分交互题的通用 hack 方法,By mcfxmcfx

有灵感在记录罢。