はじめに
ラノベのようなタイトルの通り、
ですが。。。。
「そりゃそうだろ、うまく行ってしまったら32bitアプリとの互換性が維持できないだろうさ」という冷やかな視線を感じますが(><)、せっかく試したのでまとめます。
32bitアプリの2038年問題回避方法
2038年問題とは、32bitアプリケーションでは通常"符号ありlong int"で扱うため、2038年でオーバフローする問題です。
その回避策として一般的なのが、独自に"long long int"で64bitの変数を作るか、"符号なしlong int"で処理という方法らしいです。(wikipediaによると)
- 詳細はこちらを参照-->2038年問題 - Wikipedia
標準のCライブラリ(glibcとか)は当然"符号ありlong int"なので、この回避策をするためには、時刻処理を何らかの形で独自実装するしかありません。
例えば下記のような方法で、補正をかけるラップ関数を実装する方法などです。(結論から言うと下記リンク先の対応が現実解だったのですが・・・)
実験のコード
コードの説明
カーネルから時刻取得するためには、clock_gettimeのシステムコールを利用します。比較で普通にglibc関数を利用する方法と、アセンブラで呼びだす両方を実行してみます。
具体的には下記コードでは、32bit/64bit環境それぞれで、下記処理をしています。
- glibcのclock_gettime()関数を利用して時刻取得する(普通の方法)
- (実験メイン)アセンブラで直接clock_gettimeシステムコールを呼びだし時刻取得する
- 32bitの場合、(a)32bitのtimespec構造体、(b)64bitのtimespec構造体(long long int)それぞれで取得する
- 64bitの場合、(a)64bitのtimespec構造体で取得する。
- なおアセンブラで呼びだす場合のtimespec構造体は、32/64bit環境のbit長差異をないことを明確にするため、32bit長ではint型、64bitでは"long long int"で独自定義している
なお、いろいろぐちゃぐちゃやっていたので、余計なコードが入っていますが、ご容赦ください。
コード(time.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> #include <sys/types.h> #define SYS_clock_gettime32 20 #define SYS_clock_gettime64 228 struct timespec32_test { int tv_sec; int tv_nsec; }; struct timespec64_test { long long int tv_sec; long long int tv_nsec; }; void printf_timespec(struct timespec *tp, int ret, char *mes) { printf("%-40s " ,mes); printf("tv_sec=%-12ld ",tp->tv_sec); printf("tv_nsec=%-12ld ",tp->tv_nsec); printf("ret=%d\n",ret); } void printf_timespec32_test(struct timespec32_test *tp, int ret, char *mes) { printf("%-40s ",mes); printf("tv_sec=%-12d ",tp->tv_sec); printf("tv_nsec=%-12d ",tp->tv_nsec); printf("ret=%d\n",ret); } void printf_timespec64_test(struct timespec64_test *tp, int ret, char *mes) { printf("%-40s ",mes); printf("tv_sec=%-12lld ",tp->tv_sec); printf("tv_nsec=%-12lld ",tp->tv_nsec); printf("ret=%d\n",ret); } int main() { struct timespec tp; struct timespec64_test tp64; #if defined(__i386__) || defined(__ILP32__) struct timespec32_test tp32; #endif pid_t pid; int ret; char *mes; /* info */ pid=getpid(); printf("PID=%d\n",pid); mes = "glibc clock_gettime-long int"; tp.tv_sec=tp.tv_nsec=0; ret = clock_gettime(CLOCK_REALTIME,&tp); printf_timespec(&tp, ret, mes); #if defined(__i386__) || defined(__ILP32__) /* 32bit application */ mes = "Assemble=>i386 int 0x80-timespec-32bit"; tp32.tv_sec=tp32.tv_nsec=0; __asm__ volatile( "int $0x80;" :"=a"(ret) :"0"(SYS_clock_gettime), "b"(CLOCK_REALTIME), "c"(&tp32) ); printf_timespec32_test(&tp32, ret, mes); mes = "Assemble=>i386 int 0x80-timespec-64bit"; tp64.tv_sec=tp64.tv_nsec=0; __asm__ volatile( "int $0x80;" :"=a"(ret) :"0"(SYS_clock_gettime), "b"(CLOCK_REALTIME), "c"(&tp64) ); printf_timespec64_test(&tp64, ret, mes); #else /* 64bit application */ mes = "Assemble=>x86_64 syscall-timespec-64bit"; tp64.tv_sec=tp64.tv_nsec=0; __asm__ volatile( "syscall;" :"=a"(ret) :"0"(SYS_clock_gettime), "D"(CLOCK_REALTIME), "S"(&tp64) :"memory" ); printf_timespec64_test(&tp64, ret, mes); #endif return(EXIT_SUCCESS); }
実行結果
64bit環境
何も細工していないので、アセンブラで呼びだしても普通に応答が帰ってきます。
$ gcc -m64 -o time64 -g3 -Wall -O0 time.c $ ./time64 PID=5434 glibc clock_gettime-long int tv_sec=1452509660 tv_nsec=790867319 ret=0 Assemble=>x86_64 syscall-timespec-64bit tv_sec=1452509660 tv_nsec=790892879 ret=0
32bit環境
32bitの時刻構造体のポインタを渡したときは普通ですが、64bitの時刻構造体のポインタを無理やり渡しても、当然おかしな結果になっているのがよく分かります(^^;
$ gcc -m32 -o time32 -g3 -Wall -O0 time.c $ ./time32 PID=5426 glibc clock_gettime-long int tv_sec=1452509515 tv_nsec=540033119 ret=0 Assemble=>i386 int 0x80-timespec-32bit tv_sec=1452509515 tv_nsec=540054759 ret=0 Assemble=>i386 int 0x80-timespec-64bit tv_sec=2319584530896488779 tv_nsec=0 ret=0
カーネル挙動を確認する
当然といえば、当然の結果ですが。。。
念のための裏付けで、カーネル内部で32bitのシステムコールが呼ばれた場合どのような挙動をするのかを、ftraceでカーネル内部でどの様な関数が呼びだされているか確認してみました。
ftrace(trace-cmd)での確認方法
以下のコマンドでカーネルトレースを取得します。最後の実行コマンドをそれぞれ"./time64"、"./time32"とすることで32/64bit両環境のトレースを取得します。
$ sudo trace-cmd record -p function_graph -g SyS_clock_gettime -g compat_SyS_clock_gettime ./time64 $ trace-cmd report
trace-cmdコマンドの詳細は(ftrace)trace-cmdでfunction_graphを使ってみる - のぴぴのメモを参照。
トレース結果
64bitと32bitを比較すると、32bitアプリから呼びだした場合は、cpmpat_sys_clock_gettime()なる関数でラップされているのが分かります。(blogの横幅の関係で、関数部分のみ記載しています)。なおカーネルは、ubuntu 14.04の"Linux version 3.19.0-43-generic (buildd@lgw01-16)"になります。
64bitアプリからのカーネルトレース(抜粋)
| sys_clock_gettime() { | posix_clock_realtime_get() { | getnstimeofday64() { | __getnstimeofday64() { | read_hpet(); | } | } | } | }
32bitアプリからのカーネルトレース(抜粋)
| compat_sys_clock_gettime() { | sys_clock_gettime() { | posix_clock_realtime_get() { | getnstimeofday64() { | __getnstimeofday64() { | read_hpet(); | } | } | } | } | compat_put_timespec() { | __compat_put_timespec(); | } | }
では、compat_sys_clock_gettime()ではどんな事をしているのでしょうか。
- compat_sys_clock_gettime()は、/kernel/compat.cに実装
- まず、sys_clock_gettime()を呼び出し(この時はtimespecは64bit。下記コードでts)
- その後、copmpat_put_timespec()で、システムコールの引数に渡すデータを、カーネル空間からユーザ空間の所定位置にコピーする。(下記コードでtp)
- で、そのcopmpat_put_timespec()内部で、64bitから32bitにデータを落としているらしい。
740 long compat_sys_clock_gettime(clockid_t which_clock, 741 struct compat_timespec __user *tp) 742 { 743 long err; 744 mm_segment_t oldfs; 745 struct timespec ts; 746 747 oldfs = get_fs(); 748 set_fs(KERNEL_DS); 749 err = sys_clock_gettime(which_clock, 750 (struct timespec __user *) &ts); 751 set_fs(oldfs); 752 if (!err && put_compat_timespec(&ts, tp)) 753 return -EFAULT; 754 return err; 755 }
実際にデータ変換(64bit->32bit)をしているのはinlineアセンブラっぽい
- arch/x86/include/asm/uaccess.h
411 #define __put_user_asm(x, addr, err, itype, rtype, ltype, errret) \ 412 asm volatile(ASM_STAC "\n" \ 413 "1: mov"itype" %"rtype"1,%2\n" \ 414 "2: " ASM_CLAC "\n" \ 415 ".section .fixup,\"ax\"\n" \ 416 "3: mov %3,%0\n" \ 417 " jmp 2b\n" \ 418 ".previous\n" \ 419 _ASM_EXTABLE(1b, 3b) \ 420 : "=r"(err) \ 421 : ltype(x), "m" (__m(addr)), "i" (errret), "0" (err))
ということで、カーネル内部のcpmpat_XXXXXXX関数で、32bitアプリ向けに64bit→32bitのデータ変換が行われているため、どうやっても32bitアプリでカーネルから64bitの時刻データを取得するのは難しそうです。