TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 23 READS

x86 ud2 명령은 왜 '2'인가 — Hyrum의 법칙이 만든 표준 크래시 명령

x86 ud2 명령은 왜 '2'인가 — Hyrum의 법칙이 만든 표준 크래시 명령
SOURCE IMAGE · HACKER NEWS

컴파일러가 만들어낸 어셈블리를 들여다보거나, API 후킹이 얽힌 크래시 덤프를 분석하다 보면 낯선 명령 하나를 만나게 된다. 바로 ud2다. 이름 그대로 '정의되지 않은(undefined)' 명령으로, 실행되면 반드시 '잘못된 오퍼코드(invalid opcode)' 예외를 일으키도록 아키텍처 차원에서 보장된다. 그런데 왜 하필 이름이 ud2일까. ud도 아니고 ud1도 아니라 '2'가 붙은 데는 사연이 있다. 마이크로소프트의 레이먼드 첸이 정리한 이 역사에는, 소프트웨어 실무자라면 곱씹어볼 만한 교훈이 담겨 있다.

컴파일러가 일부러 크래시를 심는 이유

ud2의 용도부터 짚어보자. 컴파일러는 '도달할 수 없는' 코드 지점을 표시하기 위해 이 명령을 삽입한다. 대표적인 예가 [[noreturn]]으로 표시된 함수다. 반환하지 않겠다고 선언한 함수가 어떤 이유로든 실제로 반환해 버리면, 그 뒤에 놓인 ud2가 실행되면서 프로그램이 즉시 크래시한다. 만약 이 안전장치가 없다면 실행 흐름은 다음 함수의 코드로 흘러 들어가 아무 의미 없는 명령들을 실행하게 된다. 예측 불가능한 동작으로 조용히 망가지느니, 확실하게 멈춰 세우는 편이 디버깅에도 안전에도 낫다는 판단이다.

ud0과 ud1이 먼저 있었다

원래 x86에는 '아키텍처적으로 정의되지 않은 명령'이라는 개념 자체가 없었다. 그래서 강제로 잘못된 오퍼코드 예외를 일으키고 싶었던 사람들은, 실행하면 안정적으로 예외를 던지는 바이트 시퀀스를 스스로 찾아 나섰다. 누군가는 0F FF가 그런 성질을 가진다는 것을 발견했다. 이 시퀀스는 내부적으로는 레지스터 목적지와 레지스터/메모리 소스라는 두 개의 피연산자를 받는 것처럼 디코딩됐지만, 예외가 먼저 발생하기 때문에 그 피연산자들은 실제로 쓰이지 않았다. 한편 다른 누군가는 0F B9도 똑같이 동작한다는 것을 찾아냈다. 그렇게 '0F FF파'와 '0F B9파'가 생겼지만, 둘 다 잘 작동했으니 굳이 싸울 일은 없었다.

문제는 다음 세대 프로세서에서 터졌다. 인텔이 0F FF를 더 이상 예외로 취급하지 않도록 바꿨을 가능성이 있다. 새 명령어에 그 자리를 쓰려 했거나, 여전히 정의되지 않은 채로 두되 예외 대신 임의의 동작을 하게 됐을 수도 있다. 어느 쪽이든 새 프로세서에서 일부 프로그램이 동작을 멈췄고, 고된 조사 끝에 원인이 밝혀졌다. 그 프로그램들이 0F FF가 잘못된 오퍼코드라는 사실에 의존하고 있었던 것이다. 0F B9에서도 비슷한 일이 벌어졌다.

'충분히 많은 사용자'가 남긴 교훈

이것이 바로 Hyrum의 법칙이다. '사용자가 충분히 많아지면, 관찰 가능한 모든 동작은 결국 누군가가 의존하게 된다.' 공식 명세에 없던 우연한 부작용조차, 실무 코드가 그것에 기대는 순간 사실상의 계약이 되어 버린다. 인텔은 사람들이 예외를 확실히 일으킬 방법을 원한다는 것을 깨닫고, 아예 이를 공식화하기로 했다. 그렇게 영구적으로 무효인 명령 ud2가 탄생했다. 기존의 0F FF는 소급해서 ud0으로, 0F B9는 ud1로 이름 붙었고, 새로 만든 권장 명령이 ud2라는 세 번째 자리를 차지한 것이다. 이것이 '2'의 정체다.

ud2가 더 나은 이유는 페이지 경계에 있다

ud2의 실질적 장점은 피연산자가 없는 2바이트 명령이라는 데 있다. 언뜻 ud0과 ud1도 그냥 2바이트 무효 명령으로 취급하면 되지 않느냐 싶지만, 여기엔 함정이 있다. 프로세서는 그 뒤에 이어지는 피연산자 바이트를 사용하지는 않아도 디코딩은 한다. 만약 명령 디코딩이 존재하지 않는(not-present) 메모리 페이지로 넘어가면, 잘못된 오퍼코드 예외가 아니라 접근 위반(access violation)이 발생한다. 게다가 일부 구형 프로세서는 나머지 바이트가 제대로 디코딩되는지 확인하기도 전에 0F FF를 보자마자 예외를 던졌다. 그래서 0F FF가 페이지 끝에 걸치고 다음 페이지가 없을 때, 어떤 때는 오퍼코드 예외가, 어떤 때는 접근 위반이 나오는 비일관적 동작이 발생했다. 반면 ud2의 동작은 일관적이고 아키텍처가 보장한다.

비슷한 목적으로 자주 쓰이는 0xCC(INT 3)와 비교하면 성격 차이가 분명하다. INT 3는 트랩에 가깝다. 주로 전제 조건이나 계약 위반을 알리고, 디버거가 붙어 있으면 그 지점에서 실행을 멈추게 하는 신호로 쓰인다. 반면 ud2는 '임의의 명령 실행'이 초래할 수 있는 광범위한 버그를 원천 차단하는 하드 스톱에 가깝다. 참고로 이런 '고장 시 안전하게 멈추는' 설계 철학은 오래된 것이어서, 6502에서는 0x00이 강제 중단(BRK)이었고, 사용하지 않는 RAM을 0으로 채워두면 폭주한 코드가 그 자리에서 붙잡히기도 했다. Acorn 머신은 한발 더 나아가 BRK 벡터가 뒤따르는 바이트를 오류 메시지로 읽어, 어떤 ROM이나 애플리케이션에서든 단일 인터페이스로 오류를 처리하게 했다.

오늘날 대부분의 개발자는 손으로 어셈블리를 짤 일이 없고, 컴파일러는 충분히 훌륭하다. 그럼에도 이 일화가 시사하는 바는 두 가지다. 하나는 어셈블리를 읽을 줄 아는 능력의 가치다. 컴파일러는 예측 불가능한 분기나 나쁜 캐시 지역성을 가진 잘못 선택된 알고리즘까지 고쳐주지는 않으며, 링크드 리스트를 더 빠른 벡터로 바꿔주지도 않는다. 자신의 소스가 어떤 기계어로 변환되는지 감을 가진 개발자는 성능이 중요한 코드에서 컴파일러를 도울 수 있다. 소스가 없는 상황을 분석하거나, godbolt.org 같은 도구로 컴파일러의 결과물을 음미할 때도 유용하다. 다른 하나가 바로 Hyrum의 법칙이다. 명세에 명시하지 않은 우연한 동작이라도, 사용자가 충분하면 그것은 곧 깨뜨릴 수 없는 인터페이스가 된다. ud2는 그 교훈을 CPU 명령어 하나로 박제해 놓은 셈이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://devblogs.microsoft.com/oldnewthing/20260910-00/?p=11...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...