Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 12833] ファイルの概要

このコミットは、Go言語のsyscallおよびnetパッケージにおけるLinux Netlinkメッセージの処理方法を改善し、システムのネイティブエンディアンネスを使用するように変更するものです。これにより、異なるエンディアンネスを持つアーキテクチャ(例: x86_64のリトルエンディアン、PowerPCのビッグエンディアン)上でのGoプログラムのネットワーク関連処理の正確性と互換性が向上します。

コミット

commit 35bc9d17df3876b612bf45d4715fe9fcc479768e
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Apr 4 17:41:36 2012 -0700

    syscall, net: use native endianness for Linux netlink messages
    
    Tested using 6g and gccgo on x86_64 GNU/Linux and using gccgo
    on PowerPC GNU/Linux (which is big-endian).
    
    R=golang-dev, bradfitz, mikioh.mikioh, iant
    CC=golang-dev
    https://golang.org/cl/5975073

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/35bc9d17df3876b612bf45d4715fe9fcc479768e

元コミット内容

このコミットは、Go言語の標準ライブラリ内のsyscallおよびnetパッケージにおいて、LinuxのNetlinkメッセージを扱う際のエンディアンネスの問題を修正します。具体的には、Netlinkメッセージのヘッダやデータ構造をバイト列に変換する際、またはバイト列から読み取る際に、手動でのバイト操作(シフト演算など)から、unsafe.Pointerを用いたネイティブエンディアンネスでの直接的なメモリ操作に切り替えています。これにより、異なるCPUアーキテクチャ(リトルエンディアンとビッグエンディアン)間での互換性が確保されます。

また、uint32型のフィールドをint型として扱う際の型変換に関する潜在的なバグも修正されています。

変更の背景

Go言語はクロスプラットフォーム対応を重視しており、様々なCPUアーキテクチャ上で動作します。しかし、ネットワークプロトコルやOSのシステムコールは、しばしば特定のバイトオーダー(エンディアンネス)に依存します。LinuxのNetlinkプロトコルもその一つです。

以前の実装では、Netlinkメッセージの構造体をバイト列に変換する際、またはバイト列から読み取る際に、手動でバイトシフト演算を行っていました。これは、特定のエンディアンネス(おそらくリトルエンディアンを想定)に固定された処理であり、ビッグエンディアンのシステム(例: PowerPC)で実行された場合に、データの解釈が誤る可能性がありました。

このコミットの背景には、Goプログラムがビッグエンディアンシステム上でNetlinkメッセージを正しく処理できないという問題があったと考えられます。ネイティブエンディアンネスを使用することで、Goランタイムが動作しているシステムのCPUのバイトオーダーに合わせてデータを読み書きできるようになり、クロスプラットフォームでの互換性と正確性が向上します。

前提知識の解説

エンディアンネス (Endianness)

エンディアンネスとは、コンピュータのメモリ上で複数バイトのデータを格納する際のバイト順序の規則です。主に以下の2種類があります。

  • リトルエンディアン (Little-endian): 最下位バイト(最も小さい値のバイト)が最も小さいアドレスに格納され、最上位バイト(最も大きい値のバイト)が最も大きいアドレスに格納されます。Intel x86系のCPUが採用しています。 例: 16進数 0x12345678 をリトルエンディアンで格納すると、メモリ上では 78 56 34 12 の順になります。
  • ビッグエンディアン (Big-endian): 最上位バイトが最も小さいアドレスに格納され、最下位バイトが最も大きいアドレスに格納されます。PowerPC、SPARC、ネットワークプロトコル(TCP/IPなど)が採用しています。 例: 16進数 0x12345678 をビッグエンディアンで格納すると、メモリ上では 12 34 56 78 の順になります。

異なるエンディアンネスを持つシステム間でデータをやり取りする際には、バイトオーダーの変換(ネットワークバイトオーダーへの変換など)が必要になります。

Netlinkは、Linuxカーネルとユーザー空間のプロセス間で通信を行うためのソケットベースのIPC(プロセス間通信)メカニズムです。主に、ネットワーク設定(ルーティングテーブル、インターフェース情報など)、ファイアウォール設定、プロセス管理など、様々なカーネル機能へのアクセスを提供するために使用されます。

Netlinkメッセージは、標準的なヘッダとそれに続くペイロードで構成されます。ヘッダにはメッセージの長さ、タイプ、フラグ、シーケンス番号、プロセスIDなどの情報が含まれます。これらの情報は、システムのエンディアンネスに従ってバイト列にシリアライズ/デシリアライズされます。

Go言語の syscall パッケージ

syscallパッケージは、GoプログラムからOSのシステムコールを直接呼び出すための低レベルなインターフェースを提供します。これにより、ファイルシステム操作、プロセス管理、ネットワーク通信など、OS固有の機能にアクセスできます。Netlinkプロトコルとのやり取りも、このパッケージを通じて行われます。

Go言語の net パッケージ

netパッケージは、TCP/IPネットワークプログラミングのための高レベルなインターフェースを提供します。IPアドレスの解析、ネットワークインターフェース情報の取得など、様々なネットワーク関連機能が含まれます。このパッケージは、内部的にsyscallパッケージを利用してOSのネットワーク機能と連携しています。

Go言語の unsafe パッケージと unsafe.Pointer

unsafeパッケージは、Go言語の型安全性をバイパスして、低レベルなメモリ操作を可能にするためのパッケージです。unsafe.Pointerは、任意の型のポインタを保持できる特殊なポインタ型で、C言語のvoid*に似ています。

unsafe.Pointerを使用することで、以下のような操作が可能になります。

  • 異なる型のポインタ間の変換(例: *byteから*uint32への変換)
  • ポインタとuintptr(整数型)間の変換
  • ポインタ演算

unsafeパッケージは、Goの型システムを破壊する可能性があるため、非常に注意して使用する必要があります。しかし、このコミットのように、特定のバイトオーダーに依存しないネイティブなメモリ操作を行う場合や、C言語の構造体とGoの構造体を直接マッピングする場合など、パフォーマンスやOSとの連携のために必要となることがあります。

技術的詳細

このコミットの主要な技術的変更点は、Netlinkメッセージのデータ構造とバイト列間の変換において、unsafe.Pointerと型キャストを積極的に利用することで、システムのネイティブエンディアンネスに依存した処理を実現している点です。

src/pkg/net/interface_linux.go の変更

  • ifi.MTU の読み取り: 変更前: ifi.MTU = int(uint32(a.Value[3])<<24 | uint32(a.Value[2])<<16 | uint32(a.Value[1])<<8 | uint32(a.Value[0])) 変更後: ifi.MTU = int(*(*uint32)(unsafe.Pointer(&a.Value[:4][0]))) 以前は、a.ValueというバイトスライスからMTU値を手動でバイトシフトしてuint32を構築していました。これはリトルエンディアンを前提とした操作です。変更後は、a.Value[:4][0](バイトスライスの最初の4バイトの先頭アドレス)をunsafe.Pointer*uint32にキャストし、そのポインタが指す値を直接読み取っています。これにより、CPUがネイティブにサポートするエンディアンネスで4バイトをuint32として解釈します。

  • /proc/net/igmp からのIPアドレス読み取り: 変更前: ifma := IPAddr{IP: IPv4(b[3], b[2], b[1], b[0])} 変更後: i := *(*uint32)(unsafe.Pointer(&b[:4][0])) ifma := IPAddr{IP: IPv4(byte(i>>24), byte(i>>16), byte(i>>8), byte(i))} /proc/net/igmpファイルから読み取られるIPアドレスは、Linuxカーネルによってネイティブエンディアンネスで格納されています。変更前は、手動でバイト順を逆にしてIPv4関数に渡していました。変更後は、まずunsafe.Pointerを使ってネイティブエンディアンネスでuint32として読み取り、その後、IPv4関数が期待するネットワークバイトオーダー(ビッグエンディアン)に変換するために、再度バイトシフト演算を行っています。これは、カーネルからのネイティブエンディアンネスのデータを、Goのnetパッケージが期待するネットワークバイトオーダーに変換する明確な意図を示しています。

  • NetlinkRouteRequest.toWireFormat() メソッド: このメソッドは、NetlinkRouteRequest構造体をNetlinkメッセージのバイト列に変換する役割を担っています。 変更前は、rr.Headerの各フィールド(Len, Type, Flags, Seq, Pid)を、手動でバイトシフトして[]byteスライスに1バイトずつ書き込んでいました。これは、特定のエンディアンネス(おそらくリトルエンディアン)に固定されたシリアライズ処理でした。 変更後:

    *(*uint32)(unsafe.Pointer(&b[0:4][0])) = rr.Header.Len
    *(*uint16)(unsafe.Pointer(&b[4:6][0])) = rr.Header.Type
    *(*uint16)(unsafe.Pointer(&b[6:8][0])) = rr.Header.Flags
    *(*uint32)(unsafe.Pointer(&b[8:12][0])) = rr.Header.Seq
    *(*uint32)(unsafe.Pointer(&b[12:16][0])) = rr.Header.Pid
    

    ここでは、バイトスライスの特定のアドレス(例: &b[0:4][0])をunsafe.Pointer*uint32*uint16にキャストし、そのポインタを通じて直接rr.Headerのフィールド値を代入しています。これにより、Goランタイムが動作しているCPUのネイティブエンディアンネスで、構造体のフィールド値がバイト列に書き込まれます。これは、Netlinkプロトコルがシステムのネイティブエンディアンネスを使用するという前提に基づいています。

  • ParseNetlinkMessage, netlinkMessageHeaderAndData, ParseNetlinkRouteAttr, netlinkRouteAttrAndValue の変更: これらの関数では、h.Lena.Lenといったuint32またはuint16型のフィールドを、int型に明示的にキャストする変更が行われています。 例: if h.Len < NLMSG_HDRLEN || int(h.Len) > len(buf) から if int(h.Len) < NLMSG_HDRLEN || int(h.Len) > len(buf) これは、h.Lenuint32型であるため、len(buf)int型)との比較や、NLMSG_HDRLENint型)との減算などの算術演算を行う際に、型の一貫性を保つための修正です。Goでは異なる数値型間の直接的な比較や演算はエラーとなるため、明示的な型変換が必要です。これにより、潜在的な型不一致によるバグや、uint32の最大値を超えた場合に負の値として扱われるなどの予期せぬ挙動を防ぎます。

コアとなるコードの変更箇所

src/pkg/net/interface_linux.go

--- a/src/pkg/net/interface_linux.go
+++ b/src/pkg/net/interface_linux.go
@@ -64,7 +64,7 @@ func newLink(ifim *syscall.IfInfomsg, attrs []syscall.NetlinkRouteAttr) Interfac
 		case syscall.IFLA_IFNAME:
 			ifi.Name = string(a.Value[:len(a.Value)-1])
 		case syscall.IFLA_MTU:
-			ifi.MTU = int(uint32(a.Value[3])<<24 | uint32(a.Value[2])<<16 | uint32(a.Value[1])<<8 | uint32(a.Value[0]))
+			ifi.MTU = int(*(*uint32)(unsafe.Pointer(&a.Value[:4][0])))
 		}
 	}
 	return ifi
@@ -193,10 +193,14 @@ func parseProcNetIGMP(path string, ifi *Interface) []Addr {
 			name = f[1]
 		case len(f[0]) == 8:
 			if ifi == nil || name == ifi.Name {
+				// The Linux kernel puts the IP
+				// address in /proc/net/igmp in native
+				// endianness.
 				for i := 0; i+1 < len(f[0]); i += 2 {
 					b[i/2], _ = xtoi2(f[0][i:i+2], 0)
 				}
-				ifma := IPAddr{IP: IPv4(b[3], b[2], b[1], b[0])}
+				i := *(*uint32)(unsafe.Pointer(&b[:4][0]))
+				ifma := IPAddr{IP: IPv4(byte(i>>24), byte(i>>16), byte(i>>8), byte(i))}
 				ifmat = append(ifmat, ifma.toAddr())
 			}
 		}
--- a/src/pkg/syscall/netlink_linux.go
+++ b/src/pkg/syscall/netlink_linux.go
@@ -30,29 +30,18 @@ type NetlinkRouteRequest struct {
 
 func (rr *NetlinkRouteRequest) toWireFormat() []byte {
 	b := make([]byte, rr.Header.Len)
-	b[0] = byte(rr.Header.Len)
-	b[1] = byte(rr.Header.Len >> 8)
-	b[2] = byte(rr.Header.Len >> 16)
-	b[3] = byte(rr.Header.Len >> 24)
-	b[4] = byte(rr.Header.Type)
-	b[5] = byte(rr.Header.Type >> 8)
-	b[6] = byte(rr.Header.Flags)
-	b[7] = byte(rr.Header.Flags >> 8)
-	b[8] = byte(rr.Header.Seq)
-	b[9] = byte(rr.Header.Seq >> 8)
-	b[10] = byte(rr.Header.Seq >> 16)
-	b[11] = byte(rr.Header.Seq >> 24)
-	b[12] = byte(rr.Header.Pid)
-	b[13] = byte(rr.Header.Pid >> 8)
-	b[14] = byte(rr.Header.Pid >> 16)
-	b[15] = byte(rr.Header.Pid >> 24)
+	*(*uint32)(unsafe.Pointer(&b[0:4][0])) = rr.Header.Len
+	*(*uint16)(unsafe.Pointer(&b[4:6][0])) = rr.Header.Type
+	*(*uint16)(unsafe.Pointer(&b[6:8][0])) = rr.Header.Flags
+	*(*uint32)(unsafe.Pointer(&b[8:12][0])) = rr.Header.Seq
+	*(*uint32)(unsafe.Pointer(&b[12:16][0])) = rr.Header.Pid
 	b[16] = byte(rr.Data.Family)
 	return b
 }
 
 func newNetlinkRouteRequest(proto, seq, family int) []byte {
 	rr := &NetlinkRouteRequest{}
-	rr.Header.Len = NLMSG_HDRLEN + SizeofRtGenmsg
+	rr.Header.Len = uint32(NLMSG_HDRLEN + SizeofRtGenmsg)
 	rr.Header.Type = uint16(proto)
 	rr.Header.Flags = NLM_F_DUMP | NLM_F_REQUEST
 	rr.Header.Seq = uint32(seq)
@@ -156,7 +145,7 @@ func ParseNetlinkMessage(buf []byte) ([]NetlinkMessage, error) {
 		}
 		m := NetlinkMessage{}
 		m.Header = *h
-		m.Data = dbuf[:h.Len-NLMSG_HDRLEN]
+		m.Data = dbuf[:int(h.Len)-NLMSG_HDRLEN]
 		msgs = append(msgs, m)
 		buf = buf[dlen:]
 	}
@@ -166,7 +155,7 @@ func ParseNetlinkMessage(buf []byte) ([]NetlinkMessage, error) {
 
 func netlinkMessageHeaderAndData(buf []byte) (*NlMsghdr, []byte, int, error) {
 	h := (*NlMsghdr)(unsafe.Pointer(&buf[0]))
-	if h.Len < NLMSG_HDRLEN || int(h.Len) > len(buf) {
+	if int(h.Len) < NLMSG_HDRLEN || int(h.Len) > len(buf) {
 		return nil, nil, 0, EINVAL
 	}
 	return h, buf[NLMSG_HDRrlen:], nlmAlignOf(int(h.Len)), nil
@@ -209,7 +198,7 @@ func ParseNetlinkRouteAttr(msg *NetlinkMessage) ([]NetlinkRouteAttr, error) {
 		}
 		ra := NetlinkRouteAttr{}
 		ra.Attr = *a
-		ra.Value = vbuf[:a.Len-SizeofRtAttr]
+		ra.Value = vbuf[:int(a.Len)-SizeofRtAttr]
 		attrs = append(attrs, ra)
 		buf = buf[alen:]
 	}
@@ -219,7 +208,7 @@ func ParseNetlinkRouteAttr(msg *NetlinkMessage) ([]NetlinkRouteAttr, error) {
 
 func netlinkRouteAttrAndValue(buf []byte) (*RtAttr, []byte, int, error) {
 	h := (*RtAttr)(unsafe.Pointer(&buf[0]))
-	if h.Len < SizeofRtAttr || int(h.Len) > len(buf) {
+	if int(h.Len) < SizeofRtAttr || int(h.Len) > len(buf) {
 		return nil, nil, 0, EINVAL
 	}
 	return h, buf[SizeofRtAttr:], rtaAlignOf(int(h.Len)), nil

コアとなるコードの解説

src/pkg/net/interface_linux.go

  • ifi.MTU = int(*(*uint32)(unsafe.Pointer(&a.Value[:4][0]))): この行は、ネットワークインターフェースのMTU (Maximum Transmission Unit) をNetlink属性から読み取る部分です。 a.ValueはNetlink属性の生データを含むバイトスライスです。a.Value[:4]は最初の4バイトを切り出します。&a.Value[:4][0]は、その4バイトの先頭アドレスを取得します。 unsafe.Pointer(...)は、このアドレスを型安全ではないポインタに変換します。 (*uint32)(...)は、そのunsafe.Pointer*uint32型(uint32へのポインタ)にキャストします。 *(*uint32)(...)は、そのuint32へのポインタが指す実際のuint32値を取得します。この操作により、CPUのネイティブエンディアンネスで4バイトがuint32として解釈されます。 最後にint(...)uint32値をintに変換し、ifi.MTUに代入しています。

  • i := *(*uint32)(unsafe.Pointer(&b[:4][0])) および ifma := IPAddr{IP: IPv4(byte(i>>24), byte(i>>16), byte(i>>8), byte(i))}: これは/proc/net/igmpから読み取ったIPアドレスを処理する部分です。 b[:4][0]は、IPアドレスの最初の4バイトの先頭アドレスです。 前のMTUの例と同様に、*(*uint32)(unsafe.Pointer(&b[:4][0]))によって、ネイティブエンディアンネスでIPアドレスがuint32として読み取られ、変数iに格納されます。 その後のIPv4(byte(i>>24), byte(i>>16), byte(i>>8), byte(i))では、読み取ったuint32iを、ネットワークバイトオーダー(ビッグエンディアン)に変換してIPv4関数に渡しています。これは、IPv4関数がビッグエンディアンのバイト順を期待するためです。この二段階の処理により、カーネルがネイティブエンディアンネスで提供するIPアドレスを、Goのネットワークスタックが正しく解釈できるようになります。

  • func (rr *NetlinkRouteRequest) toWireFormat() []byte 内の変更: この関数は、Netlinkリクエストヘッダをバイト列にシリアライズします。 変更前は、rr.Header.Lenなどのuint32uint16の値を、手動でバイトシフトしてbスライスに1バイトずつ書き込んでいました。これは、特定のエンディアンネス(おそらくリトルエンディアン)に依存していました。 変更後では、*(*uint32)(unsafe.Pointer(&b[0:4][0])) = rr.Header.Len のように、バイトスライスの特定のアドレスを対応する型のポインタにキャストし、そのポインタを通じて直接rr.Headerのフィールド値を代入しています。これにより、Goが動作しているCPUのネイティブエンディアンネスで、uint32uint16の値がメモリに書き込まれます。Netlinkプロトコルはネイティブエンディアンネスを使用するため、この変更は正しい動作を保証します。

  • rr.Header.Len = uint32(NLMSG_HDRLEN + SizeofRtGenmsg): newNetlinkRouteRequest関数内で、rr.Header.Lenに値を設定する際に、NLMSG_HDRLEN + SizeofRtGenmsgの計算結果を明示的にuint32にキャストしています。これは、rr.Header.Lenuint32型であるため、型の一貫性を保つための修正です。

  • if int(h.Len) < NLMSG_HDRLEN || int(h.Len) > len(buf) などの変更: ParseNetlinkMessage, netlinkMessageHeaderAndData, ParseNetlinkRouteAttr, netlinkRouteAttrAndValue 関数において、h.Lena.Lenといったuint32またはuint16型のフィールドを、int型に明示的にキャストしています。 Go言語では、異なる数値型(例: uint32int)を直接比較したり、算術演算を行ったりすることはできません。len(buf)int型を返すため、h.Lenintにキャストすることで、比較や減算が正しく行われるようになります。これにより、型不一致によるコンパイルエラーや、予期せぬランタイムエラーを防ぎます。

これらの変更は、Go言語が異なるCPUアーキテクチャ上でLinuxのNetlinkプロトコルと正しく連携するために不可欠なものであり、unsafe.Pointerを適切に使用することで、低レベルなメモリ操作とクロスプラットフォーム互換性を両立させています。

関連リンク

参考にした情報源リンク