のぴぴのメモ

自分用のLinuxとかの技術メモ

32bitアプリの2038年問題に対してglibc介さずシステムコールを直接呼べば何とかなるかなという浅はかな考えがやっぱり浅はかだった件

はじめに

ラノベのようなタイトルの通り、
ですが。。。。

「そりゃそうだろ、うまく行ってしまったら32bitアプリとの互換性が維持できないだろうさ」という冷やかな視線を感じますが(><)、せっかく試したのでまとめます。

32bitアプリの2038年問題回避方法

2038年問題とは、32bitアプリケーションでは通常"符号ありlong int"で扱うため、2038年でオーバフローする問題です。
その回避策として一般的なのが、独自に"long long int"で64bitの変数を作るか、"符号なしlong int"で処理という方法らしいです。(wikipediaによると)

標準のCライブラリ(glibcとか)は当然"符号ありlong int"なので、この回避策をするためには、時刻処理を何らかの形で独自実装するしかありません。
例えば下記のような方法で、補正をかけるラップ関数を実装する方法などです。(結論から言うと下記リンク先の対応が現実解だったのですが・・・)

今回の実験の安直な発想

ところで、昨今のlinuxサーバ(RHEL)は64bitが普通になっており、その場合カーネル内部では時刻処理も64bitで処理されています。(long変数は、32bit環境の場合は32bit、64bit環境の場合は64bitのため)
そこでカーネルから時刻情報を取得する場合は、32bit環境で、直接カーネル内部の64bit時刻を取得することはできないかなぁ~と思い、安直にCライブラリを使わずアセンブラで直接呼びだしてみたら何かできないかなと思った次第です。

実験のコード

コードの説明

カーネルから時刻取得するためには、clock_gettimeのシステムコールを利用します。比較で普通にglibc関数を利用する方法と、アセンブラで呼びだす両方を実行してみます。
具体的には下記コードでは、32bit/64bit環境それぞれで、下記処理をしています。

  1. glibcのclock_gettime()関数を利用して時刻取得する(普通の方法)
  2. (実験メイン)アセンブラで直接clock_gettimeシステムコールを呼びだし時刻取得する
    1. 32bitの場合、(a)32bitのtimespec構造体、(b)64bitのtimespec構造体(long long int)それぞれで取得する
    2. 64bitの場合、(a)64bitのtimespec構造体で取得する。
    3. なおアセンブラで呼びだす場合の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の時刻データを取得するのは難しそうです。