Rust프로그래밍Rust 시스템 개발자

`#[repr(packed)]` 구조체에서 필드 접근으로 유발되는 정의되지 않은 동작 조건을 자격을 부여하고, 그러한 유형 내에서 잠재적으로 정렬되지 않은 데이터를 안전하게 조작하기 위한 올바른 방법론을 지정하십시오.

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

#[repr(packed)] 속성은 메모리 레이아웃이 하드웨어 레지스터나 네트워크 프로토콜과 같은 외부 사양과 일치해야 하는 시스템 프로그래밍 요구 사항에서 발생합니다. 이 속성은 필드 간의 패딩 바이트를 제거하여 필드를 순차적인 바이트 오프셋으로 강제합니다. 일반적으로 Rust는 참조가 포인터 유형의 요구 사항에 맞게 정렬된다고 보장하지만, 패킹된 구조체는 정렬에 관계없이 필드를 순차적인 바이트 오프셋으로 놓아, u32가 4로 나누어 떨어지지 않는 주소에 놓일 수 있습니다. 이러한 정렬되지 않은 필드에 대한 참조(& 또는 &mut)를 생성하려고 하면 즉각적으로 정의되지 않은 동작을 유발합니다. 컴파일러와 LLVM는 최적화(벡터화 또는 원자 작업)를 위해 정렬된 주소를 가정합니다. 안전하게 데이터를 접근하기 위해서는 중간 참조를 아예 생성하지 않고, 대신 addr_of!addr_of_mut! 매크로를 사용하여.raw 포인터를 직접 얻고, 그 후에 ptr::read_unaligned 또는 ptr::write_unaligned를 사용하여 정렬 가정 없이 데이터를 복사해야 합니다.

use std::ptr::{addr_of, read_unaligned}; #[repr(packed)] struct Packet { flags: u8, timestamp: u64, // 오프셋 1에서 잠재적으로 정렬되지 않음 } fn get_timestamp(p: &Packet) -> u64 { // UB: &p.timestamp는 정렬되지 않은 참조를 생성함 let raw_ptr = addr_of!(p.timestamp); unsafe { read_unaligned(raw_ptr) } }

실제 상황

이진 금융 프로토콜(FIX)에 대한 제로 카피 파서를 개발하는 과정에서, 팀은 패딩 없이 즉시 이어지는 u8 메시지 유형 뒤에 u64 타임스탬프를 맞는 구조체가 필요했습니다. 초기 구현은 필드 접근을 통해 **#[repr(packed)]**를 사용했으며, 정렬되지 않은 접근이 커널에 트랩되는 ARM 아키텍처에서 간헐적인 세그멘테이션 오류를 유발했습니다.

여러 해결책이 평가되었습니다. 첫째, 이동 및 OR 연산을 사용하는 바이트 별 수동 재구성이었습니다: 이는 정렬 문제를 제거했지만 패킷마다 상당한 CPU 오버헤드를 초래하고 감사하기 복잡한 오류에 취약한 비트 조작 로직이 도입되었습니다. 둘째, 정렬을 강제하기 위해 명시적 패딩 필드가 있는 **#[repr(C)]**를 사용하는 것이었습니다: 이는 안전성을 유지했지만 후속 필드의 바이트 오프셋을 변경하여 프로토콜 호환성을 깨뜨렸고, 전송 전에 데이터를 재배열하기 위해 비싼 메모리 복사가 필요했습니다. 셋째, **#[repr(packed)]**를 유지하되 필드에 대한 접근을 정렬되지 않은 읽기를 통해 원시 포인터로만 수행하는 것이었습니다: 이는 정렬되지 않은 필드에 대한 정의되지 않은 동작을 피하면서 정확한 메모리 레이아웃을 유지했습니다.

팀은 세 번째 접근 방식을 선택하여 **addr_of!(self.timestamp)**를 사용한 getter 메서드를 구현했습니다. 이를 통해 ptr::read_unaligned를 사용하여 타임스탬프 값을 반환했습니다. 이는 ARMx86_64에서 충돌을 제거하면서 제로 카피 아키텍처를 유지하여 바이트 재구성 접근 방식에 비해 대기 시간을 40% 줄였습니다.

후보자들이 놓치는 점

정렬되지 않은 필드에 대한 참조 생성이 정렬되지 않은 접근을 지원하는 아키텍처에서도 정의되지 않은 동작을 유발하는 이유는 무엇입니까?

x86_64 프로세서는 하드웨어에서 정렬되지 않은 로드를 허용하지만, Rust의 정의되지 않은 동작 규칙은 공격적인 최적화를 가능하게 하기 위해 하드웨어 기능보다 stricter합니다. 컴파일러가 &u32를 볼 때, 주소가 네 바이트 정렬되어 있다고 가정하므로 SIMD 명령을 방출하고 후속 정렬 검사를 최적화하거나 메모리 작업을 재배열할 수 있습니다. 이러한 가정을 위반하면—관대 한 하드웨어에서도—컴파일러가 코드를 잘못 컴파일할 수 있어, 미래의 컴파일러 버전이나 다른 아키텍처에서 충돌 또는 조용한 데이터 손상이 발생할 수 있습니다.

정렬되지 않은 구조체 필드에 적용될 때 addr_of! 매크로가 & 연산자와 의미상 어떻게 다른가요?

& 연산자는 먼저 참조를 생성한 다음 이를 원시 포인터로 변환하여 할당할 경우 즉시 정렬 유효성 검사를 트리거합니다. 반대로, **addr_of!**는 중간 참조를 생성하지 않고 직접 주소를 계산하는 내장 매크로로, 정렬 요구 사항을 완전히 우회합니다. 이 차이는 중요합니다. **addr_of!**는 정렬되지 않을 수 있는 *const T를 반환하지만, &field는 필드가 정렬되지 않은 경우 UB가 됩니다. 심지어 포인터로 즉시 캐스팅하더라도 마찬가지입니다.

정렬되지 않은 필드를 포함한 패킹된 구조체에 대한 Drop 구현이 문제시되는 이유는 무엇이며, 맞춤 파괴를 어떻게 안전하게 구현할 수 있습니까?

Drop::drop 메서드는 &mut self를 받습니다. 이는 정렬되어 있지만(구조체 자체가 전체적으로 정렬을 유지함), 개별 필드를 드롭하려면 &mut Field로 그 소멸자를 호출해야 합니다. 필드가 구조체의 시작보다 높은 정렬을 가지고 있어 정렬되지 않은 경우, Drop을 호출하기 위해 &mut Field를 만드는 것은 정의되지 않은 동작입니다. 이러한 구조체를 안전하게 드롭하기 위해선 비복사 필드를 ManuallyDrop로 감싸야 하며, 그 후 맞춤 Drop 구현에서는 **addr_of_mut!**를 통해 얻은 원시 포인터에서 ptr::read_unaligned 또는 ptr::drop_in_place를 사용하여 소멸자를 실행하도록 보장합니다. 이 과정에서 정렬되지 않은 필드에 대한 정렬된 참조를 생성해서는 안 됩니다.