C言語でクロージャ

わりとありがちなネタで。まったく関係ない講義受けてるときにふと思いついた。

#include <stdio.h>
#include <string.h>
char COUNTER_CODE[] = { 
  0x55,0x89,0xe5,0xff,
  0x05,0x00,0x00,0x00,
  0x00,0xa1,0x00,0x00, 
  0x00,0x00,0xc9,0xc3
};
/*
int falsecounter(){
  return ++*((int *)0x00);
}
*/
int (*makecounter())(){
  char *counter = (char *)malloc(16);
  unsigned int count = (unsigned int)calloc(1, 4);
  memcpy(counter, COUNTER_CODE, 16);
  memcpy(counter+5, &count, 4);
  memcpy(counter+10, &count, 4);
  return (int (*)())counter;
}
int main(){
  int (*c1)(), (*c2)();
  c1 = makecounter();
  c2 = makecounter();
  printf("%d,%d\n", (*c1)(), (*c2)());
  printf("%d,%d\n", (*c1)(), (*c1)());
  printf("%d,%d\n", (*c1)(), (*c2)());
}
% cc counter.c
% ./a.out 
1,1
3,2
4,2
%

動いた。C言語はすごいなあ。

解説

上のコードがどういうもんなのか一応解説文も書いておく。まあグローバル変数の COUNTER_CODE[] が falsecounter なんです。

int falsecounter(){
  return ++*((int *)0x00);
}
% cc -c count.c
% objdump -S count.o

count.o:     ファイル形式 elf32-i386

セクション .text の逆アセンブル:

00000000 <falsecounter>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   ff 05 00 00 00 00       incl   0x0
   9:   a1 00 00 00 00          mov    0x0,%eax
   e:   c9                      leave
   f:   c3                      ret

というわけで、たしかに関数 falsecounter の実体は 5589e5ff00000000a100000000c9c3 なるバイナリ列ですね。ただし falsecounter がインクリメントして、値を読み出してるのは (int *)0x00 なる怪しげなところ。もちろんそんなところの値を書き換えたり読み出したりなんてできるわけありません。falsecounter のバイナリ列は単なるカウンタの雛形です。
makecounter でカウンター関数用の領域を char* として16バイト(先のバイナリ列を数えると16個ですね)確保して、falsecounter のバイナリ列をコピーしてるのが「memcpy(counter, COUNTER_CODE, 16);」部分。
そしてカウンタ値の int 用に 4バイトの領域を確保し、「そのアドレス値」を unsigned int として保存。 incl, mov 命令の対象としてそのアドレス値を書き込みます。書き込む位置は 0x0 になっているところなので、counter+5 と counter+10 。
最後にキャストでコンパイラさんに「さっきの counter ってやつ、ホントは char * じゃなくて int (*)() っていう関数ポインターです」と教えてリターン。はれて makecounter から帰ってきた c1, c2 はカウンターとして利用できるというおはなしです。

なんつーか悪い子のための C言語講座という気がしてきてちょっと楽しい。当然ですが x86 でしか動かんです。MIPS とかだと関数とかはアラインとったりしなきゃいけかった記憶があるけど、x86 さんは個々の命令長からしてばらばらなんだからそんなもん無いみたいっすね。ホントか?

OS の違いぐらいだったら大丈夫かも。

% CL counter.c
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8804 for 80x86
Copyright (C) Microsoft Corp 1984-1998. All rights reserved.

counter.c
Microsoft (R) Incremental Linker Version 6.00.8447
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/out:counter.exe
counter.obj
% ./counter.exe
1,1
3,2
4,2

まあこんなに簡単なのは単なるカウンタだからですけど。

test