TrotVote
[IT 심층 분석]

USB 비디오 클래스(UVC) 1.5 규격 호환성 문제와 패킷 캡처를 통한 원인 분석 보고서

김민준 · IT 시스템 엔지니어|
웹캠이나 외부 카메라 모듈을 리눅스와 윈도우 환경에 플러그 앤 플레이로 연동하기 위해 도입하는 표준이 바로 USB Video Class 통칭 UVC 규격입니다. H.264 압축 비디오 스트리밍 기능을 명시적으로 규정한 UVC 1.5 버전으로 업그레이드한 신형 보드 프로토타입을 리덕스 검증망에 붙였더니 특정 구형 안드로이드 빌드나 임베디드 리눅스 머신에 연결하는 순간 화면이 먹통이 되어버리고 USB 버스 자체가 셧다운되는 절망적인 현상이 맞닥뜨려졌습니다. 호환성의 꽃이라 불리는 범용 USB 표준 규격을 준수했는데도 불구하고 프로토콜 교섭(Negotiation) 초기 단계에서 하드웨어가 스스로 목을 졸라버렸다는 것은 결국 표준 해석에 심각한 괴리가 존재함을 방증하는 강력한 단서였습니다. 원천적인 디버깅을 위해 우리는 와이어샤크(Wireshark)와 USB 하드웨어 프로토콜 아날라이저 장비를 보드 사이에 물리적으로 끼워넣어 비트 스니핑이라는 극한의 해부학적 분석에 착수했습니다. 분석의 첫 타겟은 USB 엔드포인트 0번을 통한 컨트롤 전송 즉 열거(Enumeration) 단계의 교섭 패킷이었습니다. 커널 로그 상에 나타난 '커밋 컨트롤 오류(Commit Control Failed)' 메시지는 기기가 초기화 후 포맷과 해상도 그리고 초당 프레임을 호스트로 제안하고 클라이언트가 이를 확정하는 시퀀스에서 무언가가 엇나갔음을 가리키고 있었습니다. 스니핑 된 헥사데이터 덤프를 UVC 1.5 규격서의 바이트 맵과 일일이 대조해가며 파헤친 결과 끔찍하게도 펌웨어 측의 비디오 프로브(Probe) 패킷 응답 구조체에서 치명적인 어긋남을 발견했습니다. 페이로드의 오프셋이 규격 대비 정확히 2바이트 밀려있었고 그 자리에 위치해야 할 대역폭(Bandwidth) 요구 수량이 엉뚱하게도 쓰레기 값으로 채워져 커널의 USB 컨트롤러를 분노케 했던 것입니다. 최신 OS의 드라이버는 이를 무시하는 예외 조항이 있었지만 구형 커널들은 스펙 불일치에 기겁하며 파이프를 파괴해버린 것이었죠. 이 허점의 발단은 펌웨어 레벨을 개발할 때 콤파일러가 구조체 패딩을 마음대로 선언해버린 이른바 메모리 배치의 비정규화 때문이었습니다. 32비트와 64비트 버스 아키텍처를 오가며 C 구조체 레지스터를 제어할 때 명시적으로 packed 애트리뷰트를 걸어두지 않은 탓에 가상의 보이지 않는 2바이트 빈 공간이 숨어들어가 페이로드를 밀어냈던 촌극이었습니다. 이에 즉각적으로 소스 코드 레벨에서 구조체를 1바이트 경계로 강제 정렬시키도록 컴파일러 지시자를 재설정하고 바이트 오더링 매크로를 삽입하여 엔디안(Endianness) 불일치 가능성까지 모조리 뿌리뽑은 뒤 펌웨어 바이너리를 재압축했습니다. 재플래싱된 보드를 하드웨어 애널라이저에 물리고 수백 번의 USB 케이블 핫플러깅 테스트를 반복한 결과 UVC 컨트롤 교섭 패킷의 헥사코드는 한 치의 흐트러짐 없이 스펙 문서의 지도와 완벽하게 일치하는 황금 비율 구조체를 토해냈습니다. 낡은 구 버전의 안드로이드 태블릿부터 특수한 임베디드 리눅스를 얹은 라즈베리파이 환경까지 화면은 한 점의 잡티나 버스 리셋 오류 없이 물 흐르듯 선명하게 스트리밍 되기 시작했습니다. 이 삽질의 시간은 결국 프로토콜 호환성이라는 웅대해 보이는 단어 이면에 존재하는 진실 즉 컴파일러가 만들어내는 1바이트 단위 패딩의 나비효과가 얼마나 무시무시한 파멸을 초래할 수 있는지 엔지니어들에게 서늘한 경고를 던지는 사건으로 남아 있습니다.