コグノスケ


link 未来から過去へ表示(*)
link 過去から未来へ表示

link もっと前
   2023年 2月 8日 ---> 2023年 1月 30日
link もっと後

2023年 2月 7日

C 言語で long を int にキャストしたらどうなるの?

目次: C 言語と libc - まとめリンク

最近 RISC-V 64bit 向けの Linux システムコールの実装を調べていました(2023年 2月 1日の日記など)。そのなかで long 型を int にキャストし、負の値として解釈している部分がありました。

C 言語に詳しい方は「あれ?」と思ったかもしれません。一見して問題なさそうな操作ですが、C 言語の仕様によれば結果は実装依存(implementation-defined)です。アーキテクチャやコンパイラ次第で結果が変わるということです。

条件は long より int の方が大きい(sizeof(long) > sizeof(int) になる)ときです。現代では 64bit 環境が該当するでしょう。32bit 環境は大抵 long と int が同じ値域なので問題は発生しません。

概要をお伝えしたところで、言語仕様を見てみましょう。

N1570 (C11 committee draft) 6.3.1.3 の定義とざっくり和訳
6.3.1.3 Signed and unsigned integers

When a value with integer type is converted to another integer type other than
_Bool, if the value can be represented by the new type, it is unchanged.

Otherwise, if the new type is unsigned, the value is converted by repeatedly
adding or subtracting one more than the maximum value that can be represented
in the new type until the value is in the range of the new type.

Otherwise, the new type is signed and the value cannot be represented in it;
either the result is implementation-defined or an implementation-defined signal
is raised.


(ざっくり和訳)

整数型の値が _Bool 以外の整数型に変換される場合、もし値が新しい型で表現できるなら
値は変化しません。

新しい型が符号なしで表現できないならば、新しい型の範囲に入るまで、新しい型が表現できる
最大値より 1 多い値を繰り返し加算または減算して値を変換します。

(訳注: パディング等も認められているのでややこしい言い方になっているが、16bit 整数で
最大値 = 0xffff だとすると、最大値 + 1 = 0x1_0000 となる。16bit 以上の上位ビットの
0 クリア処理に相当する)

新しい型が符号付きで値を表現できないならば、結果は実装定義になるか、もしくは実装定義の
シグナルが生成されます。

Linux は対象とするアーキテクチャとコンパイラが限定(GCC か Clang)されており、実装依存の動きを把握した上で使っていると思われます。承知の上というやつですね。

どの実装で使われるかわからないコードを書くなら、long から int へのキャストは常に結果が同じとは限らないので、キャストの結果で何が起きるか理解して使う必要があります。

気にしすぎても仕方がない

建前は上記の通りですが「全ての C コンパイラで動作するコード」を書く機会って、ほぼないと思います。基本的には細かい仕様を気にしすぎて何も進まなくなるより、時代で受け入れられる常識的な制約を付けて進めて、後で困ったら直せば良いんじゃない?と思います。

一方で 2038年問題のように、プログラムが書かれた時代は常識的な制約だったしみんな使っていたけど、何十年も経ってから大問題になるケースもあります。「常識的な制約 = ずっと無罪放免」かどうかは誰にもわかりません。

今の 64bit アーキテクチャに由来する時間や容量の制約は十分余裕があると思われますが、100年後の世界なんてわかりません。「誰が 64bit 決め打ちの実装にした!?100年前の奴らだと……??」って泣きながら直している人がいても不思議じゃないですよね。

編集者: すずき(更新: 2023年 2月 7日 11:58)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 6日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 6 - SYSCALL_DEFINE() を全部展開

目次: RISC-V - まとめリンク

昨日の続きです。「64bit 環境においてシステムコールの引数がレジスタ長(= 64bit)以下だった場合(例えば int が典型例)、どう扱うか?」コードの解説編です。

どうして符号拡張されるのか?逆アセンブルで説明したつもりになってはいけない、逃げてはダメだ!という熱心なあなたのために、マクロを展開しながら説明します。今回の解説は __MAP(), __SC_LONG(), __SC_CAST() マクロの意味や実装です。

SYSCALL_DEFINE() の展開

今までの SYSCALL_DEFINE(), __MAP(), __SC_LONG(), __SC_CAST() マクロを全部合体させて展開して、組み込み関数などを外すとこうなるはずです。

reboot システムコールの実装部分、最終形

asmlinkage long __se_sys_reboot(__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg))
{
	long ret = __do_sys_reboot(__MAP(4,__SC_CAST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	__MAP(4,__SC_TEST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg);
	__PROTECT(4, ret,__MAP(4,__SC_ARGS,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	return ret;
}

↓こうなる

asmlinkage long __se_sys_reboot(long magic1, long magic2, long cmd, long arg)
{
	long ret = __do_sys_reboot((int)magic1, (int)magic2, (unsigned int)cmd, (void __user *)arg));
	//__SC_TEST() による型のチェック、チェックに引っかかったらコンパイルエラー
	//__PROTECT(magic1, magic2, cmd, arg) に展開されるが RISC-V 64bit では何も意味はない
	return ret;
}

つまり __se_sys_reboot() ではシステムコールからの引数は long long, unsigned long long を除いて常に long 型の引数として扱い(__SC_LONG マクロの働き)、システムコールの本体 __do_sys_reboot() にはシステムコールが定義する本来の型にキャストして渡します(__SC_CAST マクロの働き)。

例えば musl libc の reboot() の magic1 にあたるレジスタ a0 には 0x00000000_fee1dead が渡されます。long の値として解釈すると正の数 4276215469 ですが、__do_sys_reboot() の呼び出し時に int にキャストして渡すため 0xfee1dead = -18751827 であると解釈されます(たぶん、大抵は)。

キャストされた負の int 型の値を __do_sys_reboot() は long 型の引数で受け取ります。このとき long 型で -18751827 と解釈される値(= 0xfee1dead を 64bit に符号拡張した 0xffffffff_fee1dead)をレジスタ a0 に格納して、関数呼び出しを行います。

__do_sys_reboot() の magic1 引数の値(再掲)
(gdb) b __do_sys_reboot
Breakpoint 1 at 0xffffffff8002d860: file kernel/reboot.c, line 702.

(gdb) c
Continuing.

Breakpoint 1, __do_sys_reboot (magic1=-18751827, magic2=672274793,
    cmd=1126301404, arg=0x4321fedc) at kernel/reboot.c:702
702     {

(gdb) p/x $a0
$1 = 0xfffffffffee1dead

なので __do_sys_reboot() でブレークしてレジスタ a0 の内容を表示すると、0xffffffff_fee1dead になっていたわけです。

いやー長かった。マクロの魔術ですね。

編集者: すずき(更新: 2023年 2月 6日 00:02)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 5日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 5 - __MAP(), __SC_X() マクロの展開

目次: RISC-V - まとめリンク

昨日の続きです。「64bit 環境においてシステムコールの引数がレジスタ長(= 64bit)以下だった場合(例えば int が典型例)、どう扱うか?」コードの解説編です。

どうして符号拡張されるのか?逆アセンブルで説明したつもりになってはいけない、逃げてはダメだ!という熱心なあなたのために、マクロを展開しながら説明します。今回の解説は __MAP(), __SC_LONG(), __SC_CAST() マクロの展開結果です。

__MAP(), __SC_LONG(), __SC_CAST() マクロの合わせ技

今まで説明してきた __MAP(), __SC_LONG(), __SC_CAST() マクロを総動員して全部展開すると、

__MAP4(), __SC_LONG(), __SC_CAST() マクロの展開結果の例

__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)

↓

__SC_LONG(int, magic1),
__SC_LONG(int, magic2),
__SC_LONG(unsigned int, cmd),
__SC_LONG(void __user *, arg)

↓

long magic1,
long magic2,
long cmd,
long arg


__MAP(4,__SC_CAST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)

↓

__SC_CAST(int, magic1),
__SC_CAST(int, magic2),
__SC_CAST(unsigned int, cmd),
__SC_CAST(void __user *, arg)

↓

(int)magic1,
(int)magic2,
(unsigned int)cmd,
(void __user *)arg

になります。他のマクロ __SC_TEST() と __SC_ARGS() と __PROTECT() は本筋ではないので詳細は省きますが、下記のような実装です。

__SC_TEST(), __SC_ARGS(), __PROTECT() マクロの実装

// linux/include/linux/syscalls.h

#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

#define __SC_ARGS(t, a)	a

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)


// linux/include/linux/build_bug.h

/*
 * Force a compilation error if condition is true, but also produce a
 * result (of value 0 and type int), so the expression can be used
 * e.g. in a structure initializer (or where-ever else comma expressions
 * aren't permitted).
 */
#define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); })))


// linux/include/linux/linkage.h

#ifndef __ASSEMBLY__
#ifndef asmlinkage_protect
# define asmlinkage_protect(n, ret, args...)	do { } while (0)
#endif
#endif

展開するとこうなります。

__SC_TEST(), __SC_ARGS(), __PROTECT() マクロの展開の例

__SC_TEST(AAA, BBB)

// AAA が long long でも unsigned long long でもない型で、
// long 型より大きい型(構造体とかが該当する)の場合に、
// コンパイルエラーにする。


__SC_ARG(AAA, BBB)

↓

BBB


__PROTECT(n, ret, args...)

↓

// m68k アーキテクチャでは引数をスタックに置かないようにする役目のコードになるらしい。
// 他のアーキテクチャでは下記のようになって何も意味はない。
do { } while (0)

これで不明な要素は全てなくなったはずです。続きはまた今度。

編集者: すずき(更新: 2023年 2月 5日 03:39)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 4日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 4 - __MAP(), __SC_X() マクロの実装

目次: RISC-V - まとめリンク

昨日の続きです。「64bit 環境においてシステムコールの引数がレジスタ長(= 64bit)以下だった場合(例えば int が典型例)、どう扱うか?」コードの解説編です。

どうして符号拡張されるのか?逆アセンブルで説明したつもりになってはいけない、逃げてはダメだ!という熱心なあなたのために、マクロを展開しながら説明します。今回の解説は __MAP(), __SC_LONG(), __SC_CAST() マクロの意味や実装です。

任意の引数に変換を掛ける __MAP() マクロ

鍵となるのは __MAP(4, ...) の展開後に出てくる __MAP4(m, t, a, ...) というマクロです。

__MAP() マクロの実装

// linux/include/linux/syscalls.h

/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

マクロに渡された型 t と引数 a リストのペアを 1つずつ m(t, a) のように m で変換を掛けるマクロですが、イメージしづらいと思うので展開結果を載せます。

__MAP() マクロの展開結果の例

__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)

↓

__MAP4(__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)

↓

__SC_LONG(int, magic1),
__SC_LONG(int, magic2),
__SC_LONG(unsigned int, cmd),
__SC_LONG(void __user *, arg)

実装はぱっと見では良くわかりませんが、展開結果はわかりやすいですね。

キャストの要、__SC_LONG(), __SC_CAST() マクロ

先頭の展開結果 __SC_LONG(int, magic1) に着目します。

__SC_LONG() マクロの実装

// linux/include/linux/syscalls.h

#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a

#define __TYPE_AS(t, v)	__same_type((__force t)0, v)
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))


// linux/include/linux/compiler_types.h

#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))

# define __force

ここまでの実装を踏まえて展開すると下記のようになります。

__SC_LONG() マクロの展開結果の例

__SC_LONG(int, magic1),

↓ __TYPE_IS_LL() を展開

__typeof(
  __builtin_choose_expr(
    (__same_type((int)0, 0LL) ||
     __same_type((int)0, 0ULL)), 0LL, 0L))
      magic1,

↓ __same_type を展開

__typeof(
  __builtin_choose_expr(
    (__builtin_types_compatible_p(typeof((int)0), typeof(0LL)) ||
     __builtin_types_compatible_p(typeof((int)0), typeof(0ULL))), 0LL, 0L))
       magic1,

最内側の __same_type() は引数に渡した値の型を比較するマクロです。マクロで使用する __builtin_types_compatible_p() は 1つ目と 2つ目の型が同じかどうか?を int で返す組み込み関数、typeof() は引数の型を返すキーワードです(関数ではないので値は返しません)。

従って __same_type((int)0, 0LL) は int と 0LL の型、int 型と long long int 型が等しいか比較しています。当然、等しくありません。ちなみに int は __SC_LONG(int, magic1) の最初の引数 int から来たことを思い出していただけると嬉しいです。例えば __SC_LONG(long long, magic1) であれば比較結果は等しいと判断されるでしょう。

最内側の条件式 __same_type((int)0, 0LL) || __same_type((int)0, 0ULL)) の部分は型が long long もしくは unsigned long long かどうか?を調べています。

判断結果を受け取る __builtin_choose_expr(const_exp, exp1, exp2) は、const_exp が真なら、exp1 を返し、偽なら exp2 を返す組み込み関数です。もし const_exp がコンパイル時に判定できなければ、コンパイルエラーです。

以上をまとめると __SC_LONG(AAA, BBB) は、

  • AAA が long long もしくは unsigned long long なら long long BBB
  • AAA がそれ以外の型ならば long BBB

という働きをします。また __SC_CAST() はその名の通りキャストを生成します。

__SC_CAST() マクロの実装

// linux/include/linux/syscalls.h

#define __SC_CAST(t, a)	(__force t) a

です。長いですね。続きはまた今度。

編集者: すずき(更新: 2023年 2月 4日 00:56)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 3日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 3 - SYSCALL_DEFINE マクロ

目次: RISC-V - まとめリンク

昨日の続きです。「64bit 環境においてシステムコールの引数がレジスタ長(= 64bit)以下だった場合(例えば int が典型例)、どう扱うか?」コードの解説編です。

どうして符号拡張されるのか?逆アセンブルで説明したつもりになってはいけない、逃げてはダメだ!という熱心なあなたのために、マクロを展開しながら説明します。今回の解説は SYSCALL_DEFINE4() たった 1行です。

reboot システムコールの実装

// linux/kernel/reboot.c

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
{

...


// linux/include/linux/syscalls.h

#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

下記のように展開されます。

SYSCALL_DEFINE4() の展開結果

SYSCALL_METADATA(_reboot, 4, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)
__SYSCALL_DEFINEx(4, _reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)

SYSCALL_METADATA() の方は関数の実体と無関係なので今回は無視して、__SYSCALL_DEFINEx() を見ます。

__SYSCALL_DEFINEx() マクロの実装

// linux/include/linux/syscalls.h

/*
 * The asmlinkage stub is aliased to a function named __se_sys_*() which
 * sign-extends 32-bit ints to longs whenever needed. The actual work is
 * done within __do_sys_*().
 */
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

長いマクロですね。reboot システムコールの宣言部分 __SYSCALL_DEFINE4(reboot, ...) は下記のように展開されます。

SYSCALL_DEFINE4() の展開結果

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)

↓

__diag_push();

__diag_ignore(GCC, 8, "-Wattribute-alias", "Type aliasing is used to sanitize syscall arguments");

asmlinkage long sys_reboot(__MAP(4,__SC_DECL,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg)) __attribute__((alias(__stringify(__se_sys_reboot))));

ALLOW_ERROR_INJECTION(sys_reboot, ERRNO);

static inline long __do_sys_reboot(__MAP(4,__SC_DECL,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));

asmlinkage long __se_sys_reboot(__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));

asmlinkage long __se_sys_reboot(__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg))
{
	long ret = __do_sys_reboot(__MAP(4,__SC_CAST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	__MAP(4,__SC_TEST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg);
	__PROTECT(4, ret,__MAP(4,__SC_ARGS,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	return ret;
}

__diag_pop();

static inline long __do_sys_reboot(__MAP(4,__SC_DECL,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));

色々興味深い部分(__diag_push, __diag_ignore, などなど)はありますが、関数と関係ないので無視して、符号拡張を行う __se_sys_reboot() の実装部分を見ます。

SYSCALL_DEFINE4() の関数実装部分の抜粋

asmlinkage long __se_sys_reboot(__MAP(4,__SC_LONG,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg))
{
	long ret = __do_sys_reboot(__MAP(4,__SC_CAST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	__MAP(4,__SC_TEST,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg);
	__PROTECT(4, ret,__MAP(4,__SC_ARGS,int, magic1, int, magic2, unsigned int, cmd, void __user *, arg));
	return ret;
}

この関数本体の部分がどう展開されるかを見ます。続きはまた今度。

編集者: すずき(更新: 2023年 2月 3日 09:57)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 2日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 2 - Linux の動作

目次: RISC-V - まとめリンク

昨日の続きです。64bit Linux 環境の reboot() は magic の符号拡張をしてもしなくても正常に動くという話をしました。

この話は reboot には限らないので、もっと一般化すると「64bit 環境においてシステムコールの引数がレジスタ長(= 64bit)以下だった場合(例えば int が典型例)、どう扱うか?」となります。

動作を確認するにはデバッガで追えば早いでしょう。RISC-V 64bit 向けに、

  • musl libc を使うツールチェーン(後述)
  • GNU libc を使うツールチェーン(後述)
  • linux
  • busybox
  • QEMU

を用意して起動します。符号拡張を行わない musl libc 版のツールチェーンで busybox をビルドして QEMU 起動、GDB でアタッチします。

QEMU の起動、GDB からのアタッチ
$ qemu-system-riscv64 \
  -machine virt \
  -net none \
  -nographic \
  -chardev stdio,id=con,mux=on \
  -serial chardev:con -mon chardev=con,mode=readline \
  -kernel linux/arch/riscv/boot/Image \
  -initrd busybox/initramfs.cpio \
  -s


$ riscv64-unknown-linux-gnu-gdb linux/vmlinux

(gdb) target remote :1234

...

Linux のシステムコールの実装は下記のようになります。SYSCALL_DEFINE() とはなんぞや?というところが気になると思いますが、今はひとまずシステムコール名の頭に __do_sys_ を付けた関数が定義されると理解しておけば大丈夫です。

Linux の reboot システムコールの実装


//linux/kernel/reboot.c

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
{

...

ブレークポイントを __do_sys_reboot に仕掛けて QEMU 側のコンソールで busybox poweroff コマンドを実行します。

__do_sys_reboot() の magic1 引数の値(符号拡張されている)
(gdb) b __do_sys_reboot
Breakpoint 1 at 0xffffffff8002d860: file kernel/reboot.c, line 702.

(gdb) c
Continuing.

Breakpoint 1, __do_sys_reboot (magic1=-18751827, magic2=672274793,
    cmd=1126301404, arg=0x4321fedc) at kernel/reboot.c:702
702     {

(gdb) p/x $a0
$1 = 0xfffffffffee1dead

システムコールの引数は負の数(つまり 0xffffffff_fee1dead)となっています。この関数に辿り着く前に符号拡張されるようです。

バックトレース表示
(gdb) bt
#0  __do_sys_reboot (magic1=-18751827, magic2=672274793, cmd=1126301404,
    arg=0x4321fedc) at kernel/reboot.c:702
#1  0xffffffff8002dab2 in __se_sys_reboot (magic1=<optimized out>,
    magic2=<optimized out>, cmd=<optimized out>, arg=<optimized out>)
    at kernel/reboot.c:700
#2  0xffffffff80002fe2 in handle_exception () at arch/riscv/kernel/entry.S:231
Backtrace stopped: frame did not save the PC

バックトレースを見ると __se_sys_reboot() という関数も呼ばれています。この関数を調べるためにブレークを掛けて再度実行します。

__se_sys_reboot() の magic1 引数の値(符号拡張なし)
(gdb) b __se_sys_reboot
Breakpoint 1 at 0xffffffff8002daa0: file kernel/reboot.c, line 700.

(gdb) c
Continuing.

Breakpoint 1, __se_sys_reboot (magic1=4276215469, magic2=672274793,
    cmd=1126301404, arg=1126301404) at kernel/reboot.c:700
700     SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,

(gdb) p/x $a0
$1 = 0xfee1dead

システムコール例外ハンドラに渡ってきた引数 magic はレジスタ a0 に入っていて、値は 0xfee1dead です。__do_sys_reboot() や __se_sys_reboot() 関数を定義する SYSCALL_DEFINE() はマクロのため、デバッガから見てもソースコードが表示されません。

不思議な SYSCALL_DEFINE() マクロの仕組みは後日調べるとして、とりあえず今は逆アセンブルします。

__se_sys_reboot() の逆アセンブル
(gdb) disas
Dump of assembler code for function __se_sys_reboot:
=> 0xffffffff8002daa0 <+0>:     addi    sp,sp,-16
   0xffffffff8002daa2 <+2>:     sd      s0,0(sp)
   0xffffffff8002daa4 <+4>:     sd      ra,8(sp)
   0xffffffff8002daa6 <+6>:     addi    s0,sp,16
   0xffffffff8002daa8 <+8>:     sext.w  a2,a2    ★
   0xffffffff8002daaa <+10>:    sext.w  a1,a1    ★
   0xffffffff8002daac <+12>:    sext.w  a0,a0    ★
   0xffffffff8002daae <+14>:    jal     ra,0xffffffff8002d860 <__do_sys_reboot>
   0xffffffff8002dab2 <+18>:    ld      ra,8(sp)
   0xffffffff8002dab4 <+20>:    ld      s0,0(sp)
   0xffffffff8002dab6 <+22>:    addi    sp,sp,16
   0xffffffff8002dab8 <+24>:    ret

RISC-V のアセンブラを見たことない方のために補足すると、sext.w という命令は RISC-V の符号拡張命令です。また引数は a0, a1, a2, a3, a4, a5 レジスタを経由して渡されます。すなわち __se_sys_reboot() 関数は a0, a1, a2 レジスタ(引数 magic1, magic2, cmd に相当)を符号拡張して __do_sys_reboot() 関数に渡しています。

ここで当初の疑問が解消しますね。疑問の内容は下記の通りで、

  • magic1 に 0xffffffff_fee1deadを渡す派(GNU libc)→ 符号拡張するが値は変わらず
  • magic1 に 0x00000000_fee1deadを渡す派(musl libc)→ 符号拡張して 0xffffffff_fee1dead にする

答えは途中で符号拡張しているのでどちらでも問題なかった、というわけです。なるほどね。

補足: ツールチェーンの準備

ツールチェーンのビルドは crosstool-NG などでもできますし、面倒な場合は GitHub に Ubuntu 20.04 用の *.deb ファイルを置いたのでお使いください。

GNU libc の場合は、

また musl libc の場合は、

をお使いください。

編集者: すずき(更新: 2023年 2月 2日 21:07)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年 2月 1日

RISC-V 64bit Linux とシステムコールと 32bit の引数 その 1 - libc の実装

目次: RISC-V - まとめリンク

タイトルに反して実は RISC-V はあまり関係ないですが……。GNU libc と musl libc の実装を見ていたら、64bit 環境で reboot システムコールの引数 magic に、

  • 0xffffffff_fee1deadを渡す派(GNU libc)
  • 0x00000000_fee1deadを渡す派(musl libc)

がいることに気づきました。

GNU libc と musl libc の実装

C ライブラリの reboot の関数宣言は int reboot(int cmd) となっています。musl libc の場合 0xfee1dead をキャストせず long 型の引数に渡します。符号拡張は行われず 0x00000000_fee1dead がシステムコールの引数に渡されます。

musl libc の reboot() の実装

// musl/src/linux/reboot.c

int reboot(int type)
{
	return syscall(SYS_reboot, 0xfee1dead, 672274793, type);
}


// musl/arch/riscv64/syscall_arch.h

static inline long __syscall3(long n, long a, long b, long c)
{
	register long a7 __asm__("a7") = n;
	register long a0 __asm__("a0") = a;
	register long a1 __asm__("a1") = b;
	register long a2 __asm__("a2") = c;
	__asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2))
}

一方の GNU libc の場合は、0xfee1dead を int にキャストしてからシステムコールに渡します。この値は負の数なので符号拡張が行われて 0xffffffff_fee1dead がシステムコールの引数に渡されます。

GNU libc の reboot() の実装

// glibc/sysdeps/unix/sysv/linux/reboot.c

/* Call kernel with additional two arguments the syscall requires.  */
int
reboot (int howto)
{
  return INLINE_SYSCALL (reboot, 3, (int) 0xfee1dead, 672274793, howto);
}


// glibc/sysdeps/unix/sysv/linux/riscv/sysdep.h

# define internal_syscall3(number, arg0, arg1, arg2)      		\
({ 									\
	long int _sys_result;						\
	long int _arg0 = (long int) (arg0);				\
	long int _arg1 = (long int) (arg1);				\
	long int _arg2 = (long int) (arg2);				\
									\
	{								\
	register long int __a7 asm ("a7") = number;			\
	register long int __a0 asm ("a0") = _arg0;			\
	register long int __a1 asm ("a1") = _arg1;			\
	register long int __a2 asm ("a2") = _arg2;			\
	__asm__ volatile ( 						\
	"scall\n\t" 							\
	: "+r" (__a0)							\
	: "r" (__a7), "r" (__a1), "r" (__a2)				\
	: __SYSCALL_CLOBBERS); 						\
	_sys_result = __a0;						\
	}								\
	_sys_result;							\
})

最初に気づいたときはどちらかがバグっている……?と勘違いしましたが、当たり前ですがどちらも正常に動きます。すなわち Linux 側で何か対応しているはずです。続きはまた今度。

編集者: すずき(更新: 2023年 2月 2日 21:07)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
   2023年 2月 8日 ---> 2023年 1月 30日
link もっと後

管理用メニュー

link 記事を新規作成

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDF ファイル RSS 1.0
QR コード QR コード

最終更新: 2/7 17:23

カレンダー

<2023>
<<<02>>>
---1234
567891011
12131415161718
19202122232425
262728----

最近のコメント 5件

  • link 23年01月06日
    すずき 「ありがとうございます。アップデートとかセ...」
    (更新:01/24 23:19)
  • link 23年01月06日
    カナやん 「あまり褒められた方法じゃないですが、Am...」
    (更新:01/24 21:37)
  • link 22年10月10日
    すずき 「ああー、懐かしいですね。いきなりWind...」
    (更新:10/12 00:51)
  • link 22年10月10日
    hdk 「いつ頃だったかのMicrosoft Of...」
    (更新:10/11 20:31)
  • link 22年09月19日
    すずき 「それもありますね。さらに調べていたら今回...」
    (更新:09/26 11:51)

最近の記事 3件

link もっとみる
  • link 22年12月22日
    すずき 「[x86 と ARM と RISC-V で CoreMark ] ...」
    (更新:02/07 17:23)
  • link 22年04月13日
    すずき 「[C 言語と libc - まとめリンク] 目次: C 言語と l...」
    (更新:02/07 11:59)
  • link 23年02月07日
    すずき 「[C 言語で long を int にキャストしたらどうなるの] ...」
    (更新:02/07 11:58)

こんてんつ

open/close wiki
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報