32 bit 環境前提で作成されている C プログラムを 64 bit 化する

# はじめに

オークファンでは現在以下のプログラミング言語を使用してシステムを開発しています。

* PHP
* Java
* Kotlin
* Python
* Go

しかしながら、会社設立当時は

* Perl
* C (C++ ではない)

でシステムが開発されており、現在でもコアサービスの一部でまだ現役で稼働し続けているものがあります。

Perl と C でシステムが開発されていた時代は CPU や OS は現在のような 64 bit 環境ではなく 32 bit 環境でした。主に C プログラムがバイナリデータの入出力を担当する部分に使われているのですが、32 bit プログラムでは扱えるメモリの最大値が 4 GB であるため、増え続けるデータ量に耐えきれなくなり、64 bit 化を余儀なくされました。

C 言語は 32 bit と 64 bit の環境で `long` 型まわりのサイズが異なってしまう (どうしてそうなった！) ので、64 bit 化を実施した際に行ったことを簡単なサンプルで再現して説明してみようかなと思います。

(今回は Linux 環境を前提とします。Windows 環境だと少し仕様が異なる部分があるようです。)

# 検証準備

今回の検証は Windows 10 の WSL2 上で稼働する Ubuntu 18.04 LTS で実施します。
`gcc` を使用するために開発用ライブラリ一式をインストールしておきます。

```console
$ sudo apt install build-essential
```

これだけでは 32 bit コンパイルができないので、必要な追加ライブラリもインストールしておきます。

```console
$ sudo apt install libc6-dev-i386
```

ちなみに Red Hat 系では以下を実行します。

```console
$ sudo yum install "Developer Tools"
$ sudo yum install glibc-devel.i686 libgcc.i686
```

ただし、2020 年 12 月現在で Red Hat 系である Amazon Linux と Amazon Linux 2 では `No package glibc-devel.i686 available.` となって取得できませんでした。(32 bit コンパイルをさせまいとする大いなる意志を感じます...)

当時実際に作業した記録を開発部長に見せて、これで記事を書いていいですかと確認したところ、「機密情報が多分に含まれているので削るように！」とのお言葉をいただきましたので、以下の簡単な C プログラム `make_data.c` を作成しました。

```c
#include <stdio.h>
#include <stdlib.h>

// データ用の構造体
typedef struct {
    int               int_value;
    unsigned int      u_int_value;
    long              long_value;
    unsigned long     u_long_value;
    long int          long_int_value;
    unsigned long int u_long_int_value;
} RESULT;

int main(int argc,char *argv[]) {

    // 出力ファイル名を取得
    if (argc < 2) {
        fprintf(stderr, "output file name not specified.\n");
        return EXIT_FAILURE;
    }
    char* output_file_name = argv[1];
    printf("output file name: %s\n", output_file_name);

    int length = 3;
    printf("length: %d\n", length);
    printf("sizeof(RESULT): %zu bytes\n", sizeof(RESULT));
    
    // メモリを確保
    RESULT* results = (RESULT*) malloc(sizeof(RESULT) * length);
    
    // ダミーデータをセット
    int i;
    for (i = 0; i < length; i++) {
        results[i].int_value = (int) i;
        results[i].u_int_value = (unsigned int) i;
        results[i].long_value = (long) i;
        results[i].u_long_value = (unsigned long) i;
        results[i].long_int_value = (long int) i;
        results[i].u_long_int_value = (unsigned long int) i;
    }

    // データをファイルに出力
    FILE* fp;
    if ((fp = fopen(output_file_name, "wb")) == NULL) {
        // ファイルオープンに失敗した場合
        fprintf(stderr, "failed to open %s.\n", output_file_name);
        // メモリを解放
        free(results);
        return EXIT_FAILURE;
    }
    size_t write_length = fwrite(results, sizeof(RESULT), length, fp);
    printf("write length: %zu\n", write_length);
    fclose(fp);

    // メモリを解放
    free(results);

    return EXIT_SUCCESS;
}
```

上記のプログラムは `RESULT` 構造体の配列をファイルに出力します。構造体内のメンバの型は、当時改修対象だったプログラムに存在していた `int` 型関連と `long` 型関連の型を使用しています。

# 何も対策せずに 64 bit コンパイルしてみる

まず 32 bit コンパイルして実行してみます。(~~32 bit コンパイルの場合は `printf` 内の `%ld` を `%d` に変更せよとの警告が出ますが無視します。~~ `%zu` で解決しました！ @fujitanozomu様ありがとうございます！)

```console
$ gcc -m32 -o make_data_32 make_data.c
$ ./make_data_32 result_32.dat
output file name: result_32.dat
length: 3
sizeof(RESULT): 24 bytes
write length: 3
```

次に普通にコンパイル (64 bit コンパイル) してみます。

```console
$ gcc -o make_data_64 make_data.c
$ ./make_data_64 result_64.dat
output file name: result_64.dat
length: 3
sizeof(RESULT): 40 bytes
write length: 3
```

すでにこの時点で `sizeof(RESULT)` が `24 bytes` から `40 bytes` に増加していることがわかります。
ファイルサイズを確認すると以下のようになっていました。

| ファイル名 | ファイルサイズ (bytes) | 備考 |
|---|---|---|
| `result_32.dat` | 72 | 32 bit コンパイル |
| `result_64.dat` | <font color="red">**120**</font> | 64 bit コンパイル |

何も対策をしないと 32 bit コンパイル用に作成された C プログラムを 64 bit コンパイルするとデータサイズが増加してしまうことが確認できました。

# どの部分が変化しているのか

前述のとおり long 型まわりが 32 bit と 64 bit の環境でサイズが変わってしまうので、確認のために変数のサイズを表示するだけの簡単な C プログラム `type_size.c` を作成しました。

```c
#include <stdio.h>
#include <stdint.h>

void main() {
  printf("int:               %zu byte\n", sizeof(int));
  printf("unsigned int:      %zu byte\n", sizeof(unsigned int));
  printf("long:              %zu byte\n", sizeof(long));
  printf("unsigned long:     %zu byte\n", sizeof(unsigned long));
  printf("long int:          %zu byte\n", sizeof(long int));
  printf("unsigned long int: %zu byte\n", sizeof(unsigned long int));
  printf("int32_t:           %zu byte\n", sizeof(int32_t));
  printf("uint32_t:          %zu byte\n", sizeof(uint32_t));
  printf("int64_t:           %zu byte\n", sizeof(int64_t));
  printf("uint64_t:          %zu byte\n", sizeof(uint64_t));
}
```

32 bit コンパイル

```console
$ gcc -m32 -o type_size_32 type_size.c
$ ./type_size_32
int:               4 byte
unsigned int:      4 byte
long:              4 byte
unsigned long:     4 byte
long int:          4 byte
unsigned long int: 4 byte
int32_t:           4 byte
uint32_t:          4 byte
int64_t:           8 byte
uint64_t:          8 byte
```

64 bit コンパイル

```console
$ gcc -o type_size_64 type_size.c
$ ./type_size_64
int:               4 byte
unsigned int:      4 byte
long:              8 byte
unsigned long:     8 byte
long int:          8 byte
unsigned long int: 8 byte
int32_t:           4 byte
uint32_t:          4 byte
int64_t:           8 byte
uint64_t:          8 byte
```

結果

| 型 | 32 bit | 64 bit | 備考 |
|---|---|---|---|
| `int` | 4 | 4 |  |
| `unsigned int` | 4 | 4 |  |
| `long` | 4 | <font color="red">**8**</font> | ！ |
| `unsigned long` | 4 | <font color="red">**8**</font> | ！ |
| `long int` | 4 | <font color="red">**8**</font> | ！ |
| `unsigned long int` | 4 | <font color="red">**8**</font> | ！ |
| `int32_t` | 4 | 4 |  |
| `uint32_t` | 4 | 4 |  |
| `int64_t` | 8 | 8 |  |
| `uint64_t` | 8 | 8 |  |

(32 bit 環境の `long` って `int` とサイズ変わらなくてぜんぜん `long` じゃないやん！って突っ込みたくなるのは私だけでしょうか...)

上記の結果より、以下の変更を実施すれば 32 bit と 64 bit の環境に依存されることなく同一のサイズにできそうです。

- `long` → `int32_t`
- `unsigned long` → `uint32_t`
- `long int` → `int32_t`
- `unsigned long int` → `uint32_t`

# 対策を実施

`make_data.c` に対策を実施した `make_data_fixed.c` を以下のように作成しました。

```c
#include <stdio.h>
#include <stdlib.h>
// int32_t と uint32_t を使用するために追加
#include <stdint.h>

// データ用の構造体
typedef struct {
    int          int_value;
    unsigned int u_int_value;
    // 以降を変更
    int32_t      long_value;
    uint32_t     u_long_value;
    int32_t      long_int_value;
    uint32_t     u_long_int_value;
} RESULT;

int main(int argc,char *argv[]) {

    // 出力ファイル名を取得
    if (argc < 2) {
        fprintf(stderr, "output file name not specified.\n");
        return EXIT_FAILURE;
    }
    char* output_file_name = argv[1];
    printf("output file name: %s\n", output_file_name);

    int length = 3;
    printf("length: %d\n", length);
    printf("sizeof(RESULT): %zu bytes\n", sizeof(RESULT));
    
    // メモリを確保
    RESULT* results = (RESULT*) malloc(sizeof(RESULT) * length);
    
    // ダミーデータをセット
    int i;
    for (i = 0; i < length; i++) {
        results[i].int_value = (int) i;
        results[i].u_int_value = (unsigned int) i;
        results[i].long_value = (int32_t) i;
        results[i].u_long_value = (uint32_t) i;
        results[i].long_int_value = (int32_t) i;
        results[i].u_long_int_value = (uint32_t) i;
    }

    // データをファイルに出力
    FILE* fp;
    if ((fp = fopen(output_file_name, "wb")) == NULL) {
        // ファイルオープンに失敗した場合
        fprintf(stderr, "failed to open %s.\n", output_file_name);
        // メモリを解放
        free(results);
        return EXIT_FAILURE;
    }
    size_t write_length = fwrite(results, sizeof(RESULT), length, fp);
    printf("write length: %zu\n", write_length);
    fclose(fp);

    // メモリを解放
    free(results);

    return EXIT_SUCCESS;
}
```

32 bit と 64 bit それぞれでコンパイルして出力されるファイルサイズを確認してみます。

32 bit コンパイル

```console
$ gcc -m32 -o make_data_fixed_32 make_data_fixed.c
$ ./make_data_fixed_32 result_fixed_32.dat
output file name: result_fixed_32.dat
length: 3
sizeof(RESULT): 24 bytes
write length: 3
```

64 bit コンパイル

```console
$ gcc -o make_data_fixed_64 make_data_fixed.c
$ ./make_data_fixed_64 result_fixed_64.dat
output file name: result_fixed_64.dat
length: 3
sizeof(RESULT): 24 bytes
write length: 3
```

結果

| ファイル名 | ファイルサイズ (bytes) | 備考 |
|---|---|---|
| `result_32.dat` | 72 | 32 bit コンパイル (未対策) |
| `result_64.dat` | <font color="red">**120**</font> | 64 bit コンパイル (未対策) |
| `result_fixed_32.dat` | 72 | 32 bit コンパイル (対策済) |
| `result_fixed_64.dat` | <font color="red">**72**</font> | 64 bit コンパイル (対策済) |

以上の対策で無事同じデータサイズのファイルを出力できるようになりました。

# おわりに

* C プログラムを 64 bit 化する

ときいて、最初は難しいことをしないといけないのかなと思ったのですが、調べているうちに

* 構造体とポインタ変数の `long` 型まわりを文字列置換する

簡単なお仕事で実現できました。これにより、現在ほぼすべての C プログラムが 64 bit 環境への移行が完了し、正常に稼働し続けてくれています。

オークファンでは Perl と C の部分は Java や Kotlin、Go に置き換えるプロジェクトが進んでおり、これらが完了すれば今回ご紹介した対策を実施した C プログラムには引退いただけるのですが、それまであと少し頑張ってもらおうと思います。