Wireless・のおと

gccとリンクエラーのはなし

Wireless・のおと
gcc

Linux上で移植ビルドをやっているとしばしば、謎のリンクエラーに悩まされます。

start.S:84: undefined reference to `__libc_csu_init'
libcrypto.so: undefined reference to `fstat@GLIBC_2.33'
libc.so.6: undefined reference to `__tunable_is_initialized@GLIBC_PRIVATE'

Googleで検索するとQ&A事例が山のように出てきて、なんだかみんな違うことを言っていてわけがわからなかったりします。今回はこのへんの話を整理してみようと思います。

gccの基本動作

C言語の古典的なサンプルとして「Hello, world」があります。

 

hello.c:
#include <stdio.h>

int main(int argc, char *argv[])
{
        printf("Hello, world\n");
        return 0;
}

 

これをコンパイルするのに普通は

$ gcc -o hello hello.c

と書きますが、中間オブジェクトhello.oを経由する場合は

$ gcc -c -o hello.o hello.c
$ gcc -o hello hello.o

と書きます。

 

hello.oの中身をobjdumpで見るとこんな風になっています。

$ objdump --syms hello.o
SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 hello.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .note.gnu.property     0000000000000000 .note.gnu.property
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000026 main
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 puts

関数名mainがグローバルシンボル(g)として定義され、未定義の(*UND*)外部シンボルに_GLOBAL_OFFSET_TABLE_とputsが定義されています。putsは「引数なしのprintf」が自動的に置き換えられたものです。

 

生成されたELF実行ファイルhelloの中身を見ると、ずっと複雑になっています。

 

$ objdump --syms hello
SYMBOL TABLE:
0000000000000318 l    d  .interp        0000000000000000              .interp
0000000000000338 l    d  .note.gnu.property     0000000000000000              .note.gnu.property
0000000000000358 l    d  .note.gnu.build-id     0000000000000000              .note.gnu.build-id
000000000000037c l    d  .note.ABI-tag  0000000000000000              .note.ABI-tag
00000000000003a0 l    d  .gnu.hash      0000000000000000              .gnu.hash
00000000000003c8 l    d  .dynsym        0000000000000000              .dynsym
0000000000000470 l    d  .dynstr        0000000000000000              .dynstr
00000000000004f2 l    d  .gnu.version   0000000000000000              .gnu.version
0000000000000500 l    d  .gnu.version_r 0000000000000000              .gnu.version_r
0000000000000520 l    d  .rela.dyn      0000000000000000              .rela.dyn
00000000000005e0 l    d  .rela.plt      0000000000000000              .rela.plt
0000000000001000 l    d  .init  0000000000000000              .init
0000000000001020 l    d  .plt   0000000000000000              .plt
0000000000001040 l    d  .plt.got       0000000000000000              .plt.got
0000000000001050 l    d  .plt.sec       0000000000000000              .plt.sec
0000000000001060 l    d  .text  0000000000000000              .text
00000000000011e8 l    d  .fini  0000000000000000              .fini
0000000000002000 l    d  .rodata        0000000000000000              .rodata
0000000000002014 l    d  .eh_frame_hdr  0000000000000000              .eh_frame_hdr
0000000000002058 l    d  .eh_frame      0000000000000000              .eh_frame
0000000000003db8 l    d  .init_array    0000000000000000              .init_array
0000000000003dc0 l    d  .fini_array    0000000000000000              .fini_array
0000000000003dc8 l    d  .dynamic       0000000000000000              .dynamic
0000000000003fb8 l    d  .got   0000000000000000              .got
0000000000004000 l    d  .data  0000000000000000              .data
0000000000004010 l    d  .bss   0000000000000000              .bss
0000000000000000 l    d  .comment       0000000000000000              .comment
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
0000000000001090 l     F .text  0000000000000000              deregister_tm_clones
00000000000010c0 l     F .text  0000000000000000              register_tm_clones
0000000000001100 l     F .text  0000000000000000              __do_global_dtors_aux
0000000000004010 l     O .bss   0000000000000001              completed.8061
0000000000003dc0 l     O .fini_array    0000000000000000              __do_global_dtors_aux_fini_array_entry
0000000000001140 l     F .text  0000000000000000              frame_dummy
0000000000003db8 l     O .init_array    0000000000000000              __frame_dummy_init_array_entry
0000000000000000 l    df *ABS*  0000000000000000              hello.c
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
000000000000215c l     O .eh_frame      0000000000000000              __FRAME_END__
0000000000000000 l    df *ABS*  0000000000000000
0000000000003dc0 l       .init_array    0000000000000000              __init_array_end
0000000000003dc8 l     O .dynamic       0000000000000000              _DYNAMIC
0000000000003db8 l       .init_array    0000000000000000              __init_array_start
0000000000002014 l       .eh_frame_hdr  0000000000000000              __GNU_EH_FRAME_HDR
0000000000003fb8 l     O .got   0000000000000000              _GLOBAL_OFFSET_TABLE_
0000000000001000 l     F .init  0000000000000000              _init
00000000000011e0 g     F .text  0000000000000005              __libc_csu_fini
0000000000000000  w      *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000004000  w      .data  0000000000000000              data_start
0000000000000000       F *UND*  0000000000000000              puts@@GLIBC_2.2.5
0000000000004010 g       .data  0000000000000000              _edata
00000000000011e8 g     F .fini  0000000000000000              .hidden _fini
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
0000000000004000 g       .data  0000000000000000              __data_start
0000000000000000  w      *UND*  0000000000000000              __gmon_start__
0000000000004008 g     O .data  0000000000000000              .hidden __dso_handle
0000000000002000 g     O .rodata        0000000000000004              _IO_stdin_used
0000000000001170 g     F .text  0000000000000065              __libc_csu_init
0000000000004018 g       .bss   0000000000000000              _end
0000000000001060 g     F .text  000000000000002f              _start
0000000000004010 g       .bss   0000000000000000              __bss_start
0000000000001149 g     F .text  0000000000000026              main
0000000000004010 g     O .data  0000000000000000              .hidden __TMC_END__
0000000000000000  w      *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w    F *UND*  0000000000000000              __cxa_finalize@@GLIBC_2.2.5

 

hello.oで「puts」だったシンボルは「puts@@GLIBC_2.2.5」に変わっていますが、これは共有ライブラリlibc.so.6を間接参照していることを意味します。シンボル「_start」はgccが「勝手に」追加するルーチンで、OS(Linux)におけるアプリケーションの初期化・終了などの「お決まり」手続きを処理しています。この「スタートアップ・オブジェクト」は通常crt1.oというファイル名が自動的にリンクされており、Linuxでは/usr/lib/x86_64-linux-gnu/に格納されています。その中身を見てみると

$ objdump --syms /usr/lib/x86_64-linux-gnu/crt1.o
SYMBOL TABLE:
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .note.gnu.property     0000000000000000 .note.gnu.property
0000000000000000 l    d  .note.ABI-tag  0000000000000000 .note.ABI-tag
0000000000000000 l    d  .rodata.cst4   0000000000000000 .rodata.cst4
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000         *UND*  0000000000000000 __libc_csu_fini
0000000000000030 g     F .text  0000000000000005 .hidden _dl_relocate_static_pie
0000000000000000 g     F .text  000000000000002f _start
0000000000000000         *UND*  0000000000000000 __libc_csu_init
0000000000000000         *UND*  0000000000000000 main
0000000000000000  w      .data  0000000000000000 data_start
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 g     O .rodata.cst4   0000000000000004 _IO_stdin_used
0000000000000000         *UND*  0000000000000000 __libc_start_main
0000000000000000 g       .data  0000000000000000 __data_start

となっており、確かに_startが入っていてmainを外部参照しています。しかしそれだけではなく__libc_csu_initや__libc_csu_finiを外部参照しています。これらはlibc_nonshared.aに格納されています。

 

$ objdump --syms /usr/lib/x86_64-linux-gnu/libc_nonshared.a
SYMBOL TABLE:
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .note.gnu.property     0000000000000000 .note.gnu.property
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 g     F .text  0000000000000065 __libc_csu_init
0000000000000000         *UND*  0000000000000000 .hidden __init_array_start
0000000000000000         *UND*  0000000000000000 .hidden __init_array_end
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 _init
0000000000000070 g     F .text  0000000000000005 __libc_csu_fini

 

libc.so.6とlibc_nonshared.aの両方が「勝手に」リンクされるからくりは、ライブラリディレクトリに含まれている「libc.so」が名前に反して共有ライブラリファイルではなくASCII TEXTファイルで、gccに対してlibc.so.6とlibc_nonshared.aのリンクを指示するスクリプトファイルになっているからです。
 

$ cat /usr/lib/x86_64-linux-gnu/libc.so
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /lib/x86_64-linux-gnu/libc.so.6 /usr/lib/x86_64-linux-gnu/libc_nonshared.a  AS_NEEDED ( /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ) )

 

"Hello world"を動かすだけで思ったより複雑な仕掛けが動いていますが、これが今のgccとLinuxのELFバイナリ実行の仕組みです。

クロスコンパイラ環境によっては、参照先がlibc.so.6ではなくlibc.so.1で、その実体がlibuClibc-1.x.x.soにシンボリックリンクされているかも知れません。uClibc(μClibc)は標準glibcに代えて使うことのできる軽量版(機能限定版)のライブラリで、大容量メモリがまだ高価だった時代の組み込みLinuxでは多用されていました。最近は少なくなっていますが、「工業用」をうたうLinuxのBSPではまだ使われていることがあります。

よくあるトラブル

ビルド環境と実行環境が同一の場合はともかく、x86-64マシン上でaarch64バイナリを作るようなクロス環境ではしばしば、プリビルドされたrootfsイメージを用いたクロスビルドを行うことが求められます。そのクロスコンパイラを何処から持ってきて何処にインストールするかもケースバイケースです。Ubuntu Linuxでは

$ sudo apt-get install gcc-aarch64-linux-gnu

を実行することでaarch64-linux-gnu-gccが使えるようになりますが、この実行バイナリは/usr/bin/に入っており、ライブラリ群は/usr/aarch64-linux-gnu/libに入っています。この環境で

$ aarch64-linux-gnu-gcc -o hello hello.c

でビルドすればARM aarch64-ELFのhelloが作られますが、これがターゲット環境で動くとは限りません。このhelloは共有ライブラリ/usr/aarch64-linux-gnu/lib/libc.so.6とリンクすることを前提にビルドされており、ターゲットのrootfsにインストールされているlibc.so.6とバージョンが一致する保証が無いからです。もし不一致が起きると「fstat@GLIBC_2.33が見つからない」みたいな実行時エラーが出ます。

「こんなこともあろうか」と、gccにはルートディレクトリを別の場所に付け替える--sysrootオプションが用意されています。

$ aarch64-linux-gnu-gcc --sysroot=./custom-rootfs -o hello hello.c

のように指定すれば、/usr/includeや/usr/libの代わりに./custom-rootfs/usr/includeや./custom-rootfs/libが参照されるはずというオプションです。

しかしUbuntu Linuxの場合、--sysrootはネイティブコンパイラ(x86_64-linux-gnu)に対しては働くものの、どういうわけかクロスコンパイラには働きません。gcc --sysroot=で存在しないディレクトリを指定すると「stdio.hが見つからない」エラーでコンパイルも通りませんが、aarch64-linux-gnu-gccではスルーで通ってしまいます。リンク時も同様に、--sysroot(あるいは_Wl,--sysroot=)に関わらず、crt1.o・libc.so・libc.so.6・libc_nonshared.aなどのファイルは常に/usr/aarch64-linux-gnu/lib/から参照されていしまいます。

Ubuntuのクロスコンパイラで特定バージョンのlibc.so.6とリンクしたい場合は、その絶対パスを名指しで指定する必要があるようです。

$ aarch64-linux-gnu-gcc -o hello hello.c rootfs/usr/lib/aarch64-linux-gnu/libc.so.6
rootfs/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1

「ld-linux-aarch64.so.1」はこのバージョンのlibc.so.6から参照されている幾つかのシンボルを含む共有ライブラリです。gccのバージョンやrootfsのglibcのバージョンによって、このへんの組み合わせは違ってくるでしょう。めんどくさいですが「そういうもの」で、このめんどくささゆえに「Googleで検索するとQ&A事例が山のように出てきて、なんだかみんな違うことを言っている」状態になっています。

ランタイムトラブルの緊急回避

ターゲット環境で「libc.so.6のバージョンが不一致」というエラーが出て動かない場合、「本来なら」そのターゲットシステムのlibc.so.6に整合したビルドに修正すべきなのですが、「そんなことやってられん」という場合には裏技があります。Linuxは環境変数LD_LIBRARY_PATHで共有ライブラリの検索パスを指定できるので、

$ mkdir -p /home/root/lib/
$ cp libc.so.6 /home/root/lib/
$ export LD_LIBRARY_PATH=/home/root/lib
$ ./hello

のように、そのランタイムバイナリのビルドに参照された共有ライブラリをローカルディレクトリにコピーして、環境変数LD_LIBRARY_PATHにローカルディレクトリを指定して「無理やり」動かすことは可能です。これはあくまで「裏技」であり、あまり褒められたことではないことに注意してください。

その実行ファイルが必要とする共有ライブラリは「readelf」コマンドで知ることができます。

$ readelf -a hello
:
Dynamic section at offset 0x2dc8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11e8
 0x0000000000000019 (INIT_ARRAY)         0x3db8
:
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

のような表示で、”Dynamic section”からは「libc.so.6を必要としている」こと、”Symbol table”からは「putsと__libc_start_mainが外部関数(FUNC GLOBAL DEFAULT UND)として参照されている」ことがわかります。しかしながら、「どのシンボルがどのライブラリに有ることが期待されているのか」という対応一覧は無いようです。(@の後の文字列がヒントにはなります)

--sysrootオプションの謎

Ubuntuクロスコンパイラの場合、--sysrootはデフォルトライブラリ(crt1.oやlibc.so.6)のパス変更には効かない一方で、通常のリンクライブラリ(-l)の指定には効果があるようです。ここでlibsub.cというライブラリを作ってみます。

libsub.c:
int sub(void)
{
        return 42;
}

$ aarch64-linux-gnu-gcc -c -o libsub.o libsub.c
$ aarch64-linux-gnu-ar r libsub.a libsub.o

hello.cをこれを呼び出すように改変して、

include <stdio.h>

extern int sub(void);

int main(int argc, char *argv[])
{
        printf("Answer=%d\n", sub());
        return 0;
}

ビルドしようとするとエラーになります。


$ aarch64-linux-gnu-gcc -c -o hello.o hello.c
$ aarch64-linux-gnu-gcc -o hello hello.o
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: hello.o: in function `main':
hello.c:(.text+0x10): undefined reference to `sub'
collect2: error: ld returned 1 exit status
 

gccでライブラリをリンクするのは、リンクリストの中に.aないし.soの絶対パスを直接指定するか、

$ aarch64-linux-gnu-gcc -o hello hello.o libsub.a

-lでライブラリ名を指定する方法がありますが、後者は「-lsubが見つからない」というエラーになります。
 

$ aarch64-linux-gnu-gcc -o hello hello.o -lsub
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: cannot find -lsub
collect2: error: ld returned 1 exit status

gccには「インクルードパス」と「ライブラリパス」という概念があり、#includeで指定されたファイルや-lで指定されたライブラリは複数の候補を並べて指定することで、最初に見つかったファイルを使う仕組みになっています。ずいぶんいい加減な仕組みですが、1970年代に作られたカーニハン&リッチーの最初のCコンパイラがそういう仕様だったので21世紀まで引きずっています。前者は-I・後者は-Lで指定できるので、

$ aarch64-linux-gnu-gcc -o hello hello.o -lsub -L.

とすればビルドは通ります。

余談ですが、いまのconfigureやautoconfが作るMakefileでは-Iオプションが数十個くっつくことも珍しくありません。コマンドラインをいちいち表示するとコンパイル1回あたり1画面を埋め尽くすくらいのログが流れてゆくのでエコーバック表示は抑制されていることが多く、ものによって異なりますが「V=1 make」でコマンドライン表示できるものが多いです。

さてここで--sysrootの再登場です。ローカルに仮想のライブラリディレクトリを作り、そこに作成したlibsub.aをコピーして、

$ mkdir -p ./usr/lib/aarch64-linux-gnu
$ cp libsub.a usr/lib/aarch64-linux-gnu/

-lsub --sysroot=.を付けてコンパイルすれば、なんとビルドが通ります。

$ aarch64-linux-gnu-gcc -o hello hello.o -lsub --sysroot=.

--sysroot本来の機能定義「gccにとっての/の位置を差し替える」からすればおかしな話で、/が./に変わったら/usr/aarch64-linux-gnu/libに入っていたはずのcrt1.oやlibc.so.6も「見えなく」なってエラーになるはずですが、何故かaarch64-linux-gnu-gccではこれで通ってしまいます。ネイティブのx86_64 gccも#includeに対しては「そんなファイルは見つからない」とエラーにしていたのに、リンクに関しては--sysrootが効いていないようです。

このへんの挙動はgccのビルド時のconfigによって変わるのか、同じ「Linux上のクロスgcc」でもaarch64-none-linux-gccではコンパイル時もリンク時も--sysrootが律儀に解釈され、存在しないディレクトリを指定すると「crt1.oが見つからない」とエラーにするものもありました。どうしてこんなことになっているのかわかりませんが、--sysroot, -Wl,--sysroot, -L, -lは自分が使うコンパイラの癖を把握して指定する必要があるようです。

--sysrootと-Wl,--sysroot

--sysrootは「gccに対して/の挿し替えを指示するオプション」、-Wlは「gccから呼び出すリンカに対して指示するオプション」で、-Wl,--sysroot=は「リンカに対して/の挿し替えを指示するオプション」になります。--sysrootオプションが「額面通りに効く」aarch64-none-linux-gccを使ってこれらを試したとき、妙な現象に出くわしました。

 

$ aarch64-none-linux-gcc -o hello hello.c

ビルド成功。生成されたhelloには__libc_csu_initが入っている。

$ aarch64-none-linux-gcc -o hello hello.c --sysroot=./custom-rootfs

ビルド成功。生成されたhelloには__libc_csu_initが入っていない。

 

$ aarch64-none-linux-gcc -o hello hello.c -Wl,--sysroot=./custom-rootfs

ビルド失敗。「crt1.oがから参照されている__libc_csu_initが見つからない」というエラーが大量に出る。

どうやらこのクロスコンパイラでは、--sysroot=を指定した場合はcrt1.oとlibc.soがともに指定ディレクトリから参照されるのに対し、-Wl,--sysroot=を指定した場合はcrt1.oがコンパイラのデフォルトライブラリディレクトリから・libc.soが指定ディレクトリから参照されて不整合エラーになったようです。

上に書いたように、--sysrootの効き方は「ものによって違う」ので、クロスコンパイラで出た謎エラーをセルフコンパイラで再現しようとすると挙動が違ってビルド成功してしまったり、逆にクロスコンパイラでと言っていたものがセルフではエラーになったりします。めんどくさいですが「gccとはそういうもの」として付き合うしかないのかもしれません。

chrootを使う方法

gccがどうしても言うことを聞かない場合は、Linuxの”chroot”コマンドを使って「ルートディレクトリ偽装」を行う方法もあります。

$ sudo  chroot --userspec=<ユーザー名> </の代わりにしたいパス>

これを実行したあとのシェルは「指定したパスを/とみなした仮想環境の箱庭入り」になります。

chroot環境では「箱庭の外」が「見えなく」なってしまうので、ソースコードやMakefileもその仮想rootfs内にコピーしておく必要があります。chrootには不便も制限もありますが、「相対パスと絶対パスの混在問題」だとか「Makefileにおける該当パスの引用が外部アプリケーション(dpkg)に依存していて、ホスト環境とクロス環境で使い分けなければならない」のような面倒くさい問題は緩和されます。

まとめ

本業で幾つかのクロスビルド案件に関わってこの関連のエラーが出てえらく悩まされ、「そもそもcrt1.oって何処にあって何が入っているんだ」「-L, --sysroot, -Wl,--sysrootって本来の定義は何で実際のコンパイラではどう働くんだ」ということを調べ直す必要がありました。

最近のクロスビルド案件ではYoctoが使われることが多いです。YoctoはPythonスクリプトで書かれた化物みたいな自動ビルドシステムで、閉じたディレクトリ環境の中で指定されたスクリプトに従ってホストツールのリビルドから全部やり直します。1案件あたり数ギガバイトのストレージ容量を消費し、クリーンビルドからやり直すと数時間かかったりする重たいシステムですが、何から何まで指定環境に合わせてゼロから作り直すので、このような「ホストにインストールされているクロスコンパイラのlibc.so.6とターゲットrootfsのlibc.so.6のバージョン違い」のような相性問題は起きないで済みます。

しかし、「OSバージョン偽装のためのDocker」「rootfs偽装のためのchroot」「環境に応じてMakefileをカスタマイズするconfigure」「環境に応じたconfigureを生成するautomake」「特定のビルド環境をゼロから再構築するためのYocto」などを併用していると、ときどき自分が何をやっているのかわからなくなってきます。

そもそも21世紀にもなっていったい何時までネイティブバイナリコンパイラなんか使っているんだ、Java仮想マシンの仮想オブジェクトで「Write Once, Run Everywhere」とか言ってたのも30年前じゃんか!あれはどうなったんだよ!と叫びたくもなりますが、世の中得てしてそんなものであります。

 

関連記事

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください