Lobsters

Why is the x86 undefined instruction called ud2? Why 2?

x86의 미정의 명령어는 왜 ud2일까? — 왜 하필 2인가

x86 컴파일러 출력이나 크래시 덤프에서 보이는 ud2 명령어가 왜 숫자 2를 달고 있는지를 Raymond Chen이 x86 아키텍처사와 함께 설명합니다. 공식 미정의 명령어가 없던 시절 개발자들이 임의로 쓰던 0F FF와 0F B9 바이트 시퀀스가 소급해 ud0, ud1로 명명되면서, Intel이 공식으로 만든 무효 명령어가 ud2가 되었다는 이야기입니다.

본문

x86 컴파일러 출력을 보거나, 어떤 소프트웨어가 API를 디투어하다가 일으킨 크래시를 분석하다 보면 ud2라는 명령어를 만나게 됩니다. Microsoft의 Raymond Chen이 Old New Thing 블로그에서 이 명령어가 왜 'ud2'라고 불리는지, 그리고 ud1이라는 것이 있었는지를 x86 아키텍처의 역사와 함께 재구성해 설명했습니다.

ud2는 무엇인가

ud2는 아키텍처 수준에서 정의된 미정의(architecturally undefined) 명령어로, 실행되면 반드시 'invalid opcode' 예외를 발생시키는 것이 보장됩니다. 일부 컴파일러는 도달할 수 없는(unreachable) 코드를 표시하기 위해 이 명령어를 생성합니다. 실행이 어떻게든 그 지점에 도달하면 무작위 명령어를 실행하는 대신 크래시가 나도록 하기 위해서입니다. 예를 들어 [[noreturn]]으로 표시된 함수가 어떤 이유로든 반환해 버리면, 컴파일러는 호출 직후에 ud2를 배치해 프로그램이 다음 함수로 그대로 흘러 들어가는 대신 크래시가 나게 만듭니다.

ud1이 있었나 — 0F FF와 0F B9의 역사

원래 x86에는 공식적으로 정의된 미정의 명령어가 없었습니다. invalid opcode 예외를 강제로 일으키고 싶었던 사람들은 실행 시 확실히 해당 예외를 유발하는 바이트 시퀀스를 찾아 나섰고, 누군가는 0F FF 시퀀스가 invalid opcode 예외를 일으킨다는 것을 발견했습니다. 다만 이유가 무엇이든, 이 명령어는 내부적으로 레지스터 목적지와 레지스터-또는-메모리 소스라는 두 개의 피연산자를 받는 것처럼 디코딩되었습니다. 예외가 다른 무엇보다 먼저 발생하기 때문에 피연산자가 실제로 쓰이지는 않았습니다. 한편 다른 누군가는 0F B9 시퀀스도 같은 특성을 지닌다는 것을 발견했고, 0F FF 신봉자들과 0F B9 지지자들이라는 두 진영이 생겼습니다. 두 방법 모두 잘 작동했고 한쪽이 다른 쪽을 해치는 것도 아니었기 때문에 큰 경쟁은 벌어지지 않았습니다.

Hyrum의 법칙과 공식화

문제는 Intel이 다음 프로세서를 개발하면서 생겼습니다. 설계 변경으로 0F FF가 더 이상 invalid opcode 예외를 일으키지 않게 되었거나, 그 오코드를 쓰는 새 명령어를 도입하려 했을 수도 있고, 여전히 미정의지만 예외 대신 무작위 동작을 수행하게 됐을 수도 있습니다. 새 프로세서에서 소프트웨어를 실행해 보니 일부 프로그램이 동작을 멈췄고, 고된 조사 끝에 그 프로그램들이 0F FF가 무효 오코드라는 사실에 의존하고 있었다는 것이 밝혀졌습니다. 이른바 Hyrum's Law, 즉 '사용자가 충분히 많으면 관찰 가능한 모든 동작은 누군가에 의해 의존된다'는 법칙에 부딪힌 것입니다. 0F B9도 비슷한 경위로 발견됐습니다. Intel은 사람들이 invalid opcode 예외를 유발하는 신뢰할 수 있는 방법을 원한다는 것을 깨닫고 공식적인 영구 무효 명령어를 만들었습니다. 0F FF 변형을 소급해 ud0로, 0F B9 변형을 ud1로 명명하면서, 권장되는 미정의 오코드는 ud2가 된 것입니다.

왜 ud2를 써야 하는가

ud2의 장점은 피연산자가 없는 2바이트 명령어라는 점입니다. ud0나 ud1의 사용되지 않는 피연산자가 왜 문제가 되느냐 하면, 프로세서가 그것들을 실제로 사용하지는 않더라도 여전히 디코딩은 수행하기 때문입니다. 명령어 디코딩이 존재하지 않는(not-present) 페이지로 넘어가면 invalid opcode 예외가 아니라 access violation이 발생합니다. 게다가 일부 구형 프로세서는 0F FF를 디코딩하자마자 나머지 명령어가 제대로 디코딩되는지 확인하지 않고 invalid opcode 예외를 올렸기 때문에, 0F FF가 페이지 끝에 있고 다음 페이지가 없는 경우 어떤 때는 invalid opcode 예외가, 어떤 때는 access violation이 발생하는 일관되지 않은 결과가 나왔습니다. 그러니 동작이 일관되고 아키텍처적으로 보장되는 ud2를 쓰는 것이 낫다는 것이 글의 결론입니다.

Lobsters 반응

  • @regalialong — 어셈블리 배경 지식이 없어도 Raymond Chen의 글은 언제나 이해하기 쉽고 흥미로워서 늘 긍정적으로 놀라게 된다는 반응을 남겼습니다.

원문: The Old New Thing (Microsoft Dev Blogs) / 번역·요약: Trawling