享受滥用 C 语言的乐趣

c语言在线书籍

程序员导航网站

当我再次翻阅《Expert C Programming》这本书时,我偶然发现了国际模糊 C 代码竞赛的“轻松”部分。这是一场编写最晦涩难懂的代码的竞赛。C 语言有一个编写令人困惑的代码的竞赛,这可能说明了该语言的一些问题。我想看看这个竞赛的参赛作品是如何运作的。在互联网搜索中找不到任何解释,我决定自己调查一下。

IOCCC 的灵感来自 Steve Bournes 的决定,他决定在用 C 的语法编写 Unix shell 时(滥用)使用 C 预处理器,这种语法更像 Algol-68,带有明确的结束语句提示,代码如下

if
  ...
fi

他使用类似以下的代码实现了这一点

#define IF if(
#define THEN ){
#define ELSE } else {
#define FI ;}

这让他可以编写如下代码

IF *s2++ == 0
THEN return(0);
FI

正如专家 C谈到此类代码时所说;

避免使用任何修改底层语言的 C 预处理器

1987 年的早期获胜者之一是 Korn shell 的作者 David Korn 的一句俏皮话(这些 shell 作者是怎么回事):

main(){printf(&unix["\021%six\012\0"], (unix)["have"]+"fun"-0x60);}

就是这样。继续编译它。它会打印什么?

它无法在 Microsoft 上运行(提示!),但我发现它已在在线编译器ideone上,您可以尝试一下。(添加了一些内容以使其在那里运行,但其他方面相同。)

它只是打印

unix

这是从哪里来的?看起来像是一个名为的数组unix,但它没有在这里声明。是unix关键字吗?它是否以某种方式打印变量名称?

我盲目地尝试通过添加

printf(unix);

最后让它告诉我printf需要一个char *而不是一个int

将其打印出来int告诉我它的值为 1。这让我认为它是一个#define,就像将其定义为在 Unix 系统上编译一样。搜索gcc 源代码,我发现它是一个运行时目标规范。这解释了为什么它在 Windows 上不起作用。

unix就是 1。重写后我们有

main(){printf(&1["\021%six\012\0"], (1)["have"]+"fun"-0x60);}

那么这unix不是数组变量的名称,但是 1[] 如何工作?我以前见过这个,这是我最喜欢的 C 事实之一。

C 起源于 BCPL 语言。BCPL的创建者 Martin Richards 博士说道;

单值间接运算符 ! 以指针为参数并返回指向的字的内容。如果 v 是指针,!(v+I) 将访问 v+I 指向的字。… ! 的二元版本定义为 v!i = !(v+I)。v!i 的行为类似于下标表达式,其中 v 是一维数组,I 是整数下标。请注意,在 BCPL 中,v!5 = !(v+5) = !(5+v) = 5!v。在 C 中也是如此,v[5] = 5[v]。

换句话说,下标只是对指针进行加法运算,由于加法是可交换的,所以下标运算符也是可交换的。继续尝试一下。

int x[] = {1, 2, 3};
printf("%d\n%d\n", x[1], 1[x]);

那么是什么1["\021%six\012\0"]?按照我们使用下标运算符访问数组元素的正常方式编写,我们有"\021%six\012\0"[1]。仍然不典型,但你可以看到它是array[index],尽管通常不使用字符串文字。但那也行得通,也试试吧;

printf("%c\n", "hello, world"[1]);

我们在弄清楚这个问题的同时,先重写第一个数组。

main() {
  char str[] = "\021%six\012\0";
  printf(&str[1], (1)["have"]+"fun"-0x60);
}

这仍然有效。查看str,我想知道哪个\0是空字符(或 NUL 字符?)我以为 C 字符串文字默认有一个空字符。看看当我们删除它时会发生什么;

printf("%s", "\021%six\012");

印刷

█%six

我使用格式字符串,"%s"因为我尝试打印的字符串包含格式字符%。(C 编程提示:不要只打印字符串,以防printf(myStr)万一您要打印的字符串包含格式字符。%s按照上面所示进行打印。或者用作puts下面提到的注释器。)

即使没有 ,它似乎仍然可以工作\0。也许您必须在某些 ANSI C 之前的版本中将自己的空字符添加到字符串文字中?我猜不是,因为程序中的其他字符串没有它。或者那是更多的混淆?无论如何,让我们省略那个\0

既然我们已经开始了,让我们看看字符串的其余部分。 \xxx是如何以八进制给出一个字符,\021是一些控制字符和\012换行符,或者\n就像您通常在要打印的字符串末尾看到的那样。

知道\021只是一个字符,str[1]%。 &str[1]那么就是从 开始的字符串%。所以字符串实际上可以只是%six\n,省略那个控制字符,我甚至不知道它是什么意思。

main() {
  char str[] = "%six\n";
  printf(str, (1)["have"]+"fun"-0x60);
}

传递给 的第一个字符串printf是格式字符串,%s表示将下一个字符串放在其位置。由于此字符串以 结尾ix,我们可以猜测传递给 的下一个字符串printf一定是un某种字符串。这很简单,我们可以摆脱我们用来将其拉出的字符数组。

main() {
  printf("%six\n", (1)["have"]+"fun"-0x60);
}

对于下一个字符串,我们有。中(1)["have"]+"fun"-0x60有一个,现在我们必须找到它。unfun

我们再次使用 索引技巧(1)["have"]。1 周围的括号是不需要的。同样,在旧 C 中是必需的,还是更多的误导? "have"[1]给我们a。字符的十六进制值为a0x61,我们减去 0x60。然后这只是1+"fun"

与之前类似,"fun"基本上解析为char *。 向其添加 1 可得到从第二个字符开始的字符串,即un。 这变成

main() {
  printf("%six\n", "un");
}

这是未混淆的代码。

我喜欢一些更语义化的混淆,比如使用定义的单词,unix也许会试图让你误以为它#define本身就是以某种方式打印的。字符\021被反转\012可能是为了让你认为它很重要,但实际上它并没有被使用。格式字符串也可能%six包含看起来像单词“six”的内容,也许是为了让你不认为“s”被用作格式字符。

从这么少的代码中可以解开很多东西,还有很多东西需要学习。

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值