57. [StructLayout]·MemoryMarshal 재해석·엔디안 (패킷 직렬화 함의)
난이도 상내 리뷰 · C#
해설 · C#
해설 — [StructLayout]·MemoryMarshal 재해석·엔디안 (패킷 직렬화 함의)
난이도: 상
예측 출력 (x86-64, 리틀 엔디안)
1 2 3 4
True
67305985
16909060
4
요약
LayoutKind.Explicit + FieldOffset 은 필드의 바이트 오프셋을 직접 지정한다. 같은 오프셋에 여러 필드를 두면 C 의 union 처럼 메모리를 공유한다. int 를 바이트로 겹쳐 보면 나오는 순서는 호스트 엔디안(x86 은 리틀 엔디안)에 좌우된다. MemoryMarshal.Read<T> 는 바이트열을 호스트 엔디안으로 그대로 재해석하므로 이식성이 없다.
단계별 내부 동작
o.AsInt = 0x04030201후B0 B1 B2 B3→1 2 3 4. 리틀 엔디안은 최하위 바이트(0x01)를 가장 낮은 주소(offset 0=B0)에 둔다. 그래서 B0=0x01=1, B1=0x02=2, B2=0x03=3, B3=0x04=4.BitConverter.IsLittleEndian→ True. x86-64/대다수 ARM 런타임은 리틀 엔디안.MemoryMarshal.Read<int>(buf)withbuf={1,2,3,4}→ 67305985. 바이트열을 호스트 엔디안(LE)으로 int 로 재해석: 1 + 2·256 + 3·65536 + 4·16777216 = 0x04030201 = 67305985. 즉 "네트워크 순서"가 아니라 메모리 바이트를 그대로 int 로 본 값.BinaryPrimitives.ReadInt32BigEndian(buf)→ 16909060. 같은 바이트열을 빅 엔디안(네트워크 바이트 오더)으로 해석: 1·16777216 + 2·65536 + 3·256 + 4 = 0x01020304 = 16909060. 3번과 값이 다름 — 엔디안을 명시하면 결과가 결정적임을 보여준다.Marshal.SizeOf<Overlap>()→ 4. 모든 필드가 offset 0~3 에 겹쳐 있어 구조체가 차지하는 최대 범위는 4바이트.
흔한 오해·함정
- MemoryMarshal.Read 로 패킷 파싱: 호스트 엔디안으로 재해석하므로, 네트워크(빅 엔디안 관례)에서 온 바이트를 그대로 읽으면 리틀 엔디안 호스트에서 바이트가 뒤집힌 값이 된다. 서로 다른 엔디안의 두 서버가 통신하면 값이 깨진다.
- 구조체 zero-copy 재해석의 위험 3종: (1) 엔디안 — 호스트마다 다름, (2) 정렬/패딩 —
LayoutKind.Sequential은 정렬 패딩이 들어가 네트워크 레이아웃과 어긋남, (3) 필드 재배치 — 런타임/버전에 따라(Auto 레이아웃) 순서가 달라질 수 있음. 그래서 패킷을 struct 로MemoryMarshal.Cast해 직접 매핑하는 것은 이식성·안정성이 없다. - Marshal.SizeOf vs sizeof:
Marshal.SizeOf는 마샬링(네이티브 상호운용) 관점의 크기로Pack·CharSet에 좌우되고,sizeof(unsafe) 는 관리 런타임의 크기다. 둘이 다를 수 있어 혼용하면 오프셋 계산이 어긋난다.
수정 방향(네트워크 직렬화)
- 바이트↔정수 변환은 항상 엔디안을 명시한다:
BinaryPrimitives.ReadInt32BigEndian/LittleEndian,WriteUInt16BigEndian등. 프로토콜이 정한 바이트 오더로 읽고 쓴다. - 구조체를 통째로 재해석하지 말고 필드 단위로 명시적 직렬화기를 둔다. 성능이 중요하면
Span<byte>+BinaryPrimitives조합으로 할당 없이 처리한다. - 상호운용/파일 포맷 매핑에 struct 를 써야 하면
LayoutKind.Sequential, Pack=1로 패딩을 제거하고 엔디안을 별도 변환한다.
면접 포인트
- 엔디안은 "호스트가 정수를 메모리에 어떤 바이트 순서로 두는가"이고, 네트워크 프로토콜은 보통 빅 엔디안을 관례로 한다.
MemoryMarshal.Read는 호스트 엔디안 재해석이라 프로토콜 파싱에 부적합. - Explicit 레이아웃 + FieldOffset 은 union·엔디안 시연·상호운용 매핑에 쓰이지만, 네트워크 직렬화의 정답은 엔디안을 명시하는
BinaryPrimitives계열이다.