やさしいC言語入門 - 関数を作る

関数を自分で作る

C言語には関数というものがあります。C言語を勉強している人はprintf関数やscanf関数を使ったことがあると思いますが、それが関数です。さらに言うとmain関数も1つの関数です。

ざっくり言うと、関数というのは「引数を受け取って、指定された処理をこなすもの」です。「引数に100を足した数を返す」という処理を指定しておけば、その関数を呼び出すといつでも引数に100を足して返してくれます。

さて、今回は関数を自分で作ってみましょう。

全体の流れとしては、「①関数を置く領域を作る」→「②そこに処理を書き込む」→「③呼び出す」となります。今回は②のところで「4バイトの引数を受け取り、これに100を足した数を返す」とすることにしましょう。

①関数を置く領域を作る

変数の型には、「int」や「double」や「char *」などの「1つの値を保管するもの」と、「int[]」や「char[]」や構造体などの「複数の値を保管するもの」があります。関数のこなす処理はそのどちらで表されるしょうか。もちろん、複雑な処理が4バイトや8バイトで表されるわけがありませんから、後者になります。関数はたくさんの小さな処理の積み重ねでできていて、1つ1つの処理は1〜7バイト程度で表されます。関数を作るときには、これらを連続したデータ領域に書き込む必要があります。

ここで注意しなければいけないことがあります。普段連続したデータ領域を作るときには配列を作ったりmalloc/calloc関数を使ったりしますが、関数の処理を書き込む領域をこのやり方で確保しても、呼び出して実行することができません(※環境による)。ハッキング防止のためにOSが実行禁止にしているからです。

そこで、別のやり方でデータ領域を確保する必要があります。mmap関数というものを使います。

mmap関数は、malloc関数を使ってある程度の大きさのデータ領域を要求したときに内部的に呼び出される関数です。簡単に言うと、動的にデータ領域を確保する手段の1つということです。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);

mmap関数の第3引数protで、そのデータ領域に対するどんなアクセスの仕方を許可するか指定できます。PROT_READは読み出しの許可、PROT_WRITEは書き込みの許可、PROT_EXECは実行の許可です。mallocを使うとこのPROT_EXECが指定されないため、実行できないのです。PROT_READ、PROT_WRITE、PROT_EXECのうち複数を指定したいときは | でビット和をとります。何も指定しないときはPROT_NONEとします(データにアクセスできなくなります)。

さて、今回は「関数の処理を書き込む」と「実行する」の両方が必要ですから第3引数protには「PROT_WRITE | PROT_EXEC」を指定します。

また、第4引数では実際にどの場所にメモリ領域を確保するのか指定します。例えばここで"hoge"という名前のファイルを作ってそのディスクリプタを渡すと、書き込んだ内容がhogeに出力されるのですが、今回は関数を作って使うだけでその中身をファイルに残しておく必要が無いので /dev/zero を使います。open("/dev/zero", O_RDONLY);の返り値を渡せばよいです。
2018/12/04追記:O_RDWRとしていましたがO_RDONLYで良いようです。

第1引数addrはNULL。第2引数lenは確保するバイト数。第4引数flagsはMAP_PRIVATE。第6引数offは0とします。

mmap関数は確保した領域の先頭アドレスを返すので、それをchar *型の変数に入れます。これで関数を置く領域が作れました。

また、malloc関数で確保した領域をfree関数で解放するのと同じように、mmap関数で確保した領域は使い終えた後にmunmap関数という関数を使って解放します。munmap関数の第1引数には確保する領域のアドレス、第2引数には確保していたバイト数を渡します。

ここまでの流れをまとめて書くと次のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    int fd, mapsize;
    char *map;
    fd = open("/dev/zero", O_RDONLY);
    mapsize = 100;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    close(fd);
    munmap(map, mapsize);
    return 0;
}

確保する領域のサイズmapsizeは一時的に100としておきましたが、作る関数に合わせて後で変更します。

②処理を書き込む

確保した領域に実際に処理を書き込んで関数を作ります。

今回は4バイトの引数を1つ受け取り100を足して返す関数なので、「push %rbp」(55)「mov %rsp,%rbp」(48 89 e5)「mov %edi,-0x4(%rbp)」(89 7d fc)「mov -0x4(%rbp),%eax」(8b 45 fc)「add $0x64,%eax」(83 c0 64)「pop %rbp」(5d)「retq」(c3)となりますね。これをconst charの配列として用意しておいて、memcpy関数を使ってさっき作った領域にコピーします(memcpy関数を使うにはstring.hをインクルードします)。

const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
memcpy(map, data, sizeof data);

これで関数が作れました!!!

③呼び出す

mapはchar *型なので、そのままでは関数として呼び出すことができません。関数ポインタ型という型にキャストする必要があります。

fncという名前の関数ポインタ型の変数を宣言するときは次のようにします。

int型の引数を3つ受け取ってint型の値を返す関数へのポインタ:

int (*fnc)(int, int, int);

引数を受け取らずdouble *型の値を返す関数へのポインタ:

double *(*fnc)(void);

第1引数がchar[]型、第2引数がint型で、値を返さない関数へのポインタ:

void (*fnc)(char [], int);

今回はint型の引数を1つ受け取ってint型の値を返すので、int(*fnc)(int)となります。mapをキャストしてこれに代入します。

int (*fnc)(int);
fnc = (int(*)(int))map;

そのまま初期化するなら

int (*fnc)(int) = (int(*)(int))map;

のようになります。

さて、準備は整いました。いよいよ作った関数を呼び出します。

関数を呼び出すときには、演算子「( )」を使います。括弧の前に関数の名前、括弧の中に引数を入れます。

fnc(3);

試しに表示してみると、ちゃんと100が足されて返ってくるはずです(printf関数を使うにはstdio.hをインクルードします)。

printf("fnc(3) = %d\n", fnc(3));

出力:

fnc(3) = 103

ここまでをまとめると、コード全体は次のようになります:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
    int fd, mapsize;
    char *map;
    const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
    int (*fnc)(int);
    fd = open("/dev/zero", O_RDONLY);
    mapsize = sizeof data;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data, sizeof data);
    fnc = (int(*)(int))map;
    printf("fnc(3) = %d\n", fnc(3));
    close(fd);
    munmap(map, mapsize);
    return 0;
}

以上、C言語において自力で関数を作る方法を紹介しました。他の関数も作ってみたいという方は機械語について勉強してみて下さい。

ではまた