1P by GN⁺ | ★ favorite | 댓글 1개
  • Time Travel Debugging용 CPU 에뮬레이터를 C++로 다시 쓰는 과정에서, x86/amd64는 같은 동작도 인코딩·프리픽스·실행 모드에 따라 다르게 처리해야 함
  • int 3CC 단일 바이트 인코딩, ADD EAX, imm의 짧은 형식, 효과 없는 REX 프리픽스처럼 성능과 디버깅에 영향을 주는 대체 표현이 많음
  • INC/DEC, CMPXCHG8B/CMPXCHG16B, shift/rotate 명령은 플래그 처리 방식이 직관과 달라 에뮬레이터 버그로 이어지기 쉬움
  • shift count는 피연산자 크기 그대로 적용되지 않고 마스크되므로, shr eax,20h는 32비트 레지스터를 0으로 만들지 않고 값을 유지
  • 세그먼트는 32비트와 64비트 Windows의 TEB 접근에 여전히 쓰이며, FS/GS의 의미와 base 결정 방식 차이가 디스어셈블러와 에뮬레이터 구현에 직접 영향을 줌

TTD 에뮬레이터 재작성에서 드러난 x86의 세부 규칙

  • Time Travel Debugging의 한 구성 요소는 프로세스 실행 전체를 명령어 수준에서 기록하는 CPU 에뮬레이터
  • 첫 버전인 iDNA의 에뮬레이터는 거의 전부 어셈블리로 작성되어 빨랐지만, 유지보수와 확장이 어려웠음
  • 두 번째 버전에서는 에뮬레이션 부분과 이후 대부분의 다른 부분을 C++로 다시 작성했고, 어셈블리 버전의 성능 대부분을 유지하면서 더 관리하기 쉬운 코드 기반을 목표로 함
  • CPU 에뮬레이터를 만들려면 CPU 동작의 세부 사항을 빠짐없이 맞춰야 하며, x86 경험자에게 익숙한 규칙도 실제 구현에서는 다시 확인하게 됨

같은 명령을 여러 방식으로 표현하는 x86 인코딩

  • x86은 같은 명령을 여러 바이트열로 표현할 수 있음
  • int 3CD 03으로도 인코딩되지만, 단일 바이트 CC로도 인코딩 가능함
    • 소프트웨어 브레이크포인트로 쓰이기 때문에, 다음 페이지가 매핑되지 않은 메모리 페이지 끝의 명령 위치에도 브레이크포인트를 넣을 수 있음
  • 흔한 경우를 짧게 만들기 위한 대체 인코딩도 있음
    • add eax, imm05cccccccc처럼 짧게 표현 가능함
    • 같은 값을 ECX에 더하려면 81c1cccccccc처럼 1바이트가 더 필요함
  • EAX가 “Accumulator register”로 불리는 것은 관례만이 아니라 실제 인코딩 차이를 만들며, 짧은 명령은 메인 메모리에서 옮겨야 할 데이터와 명령 캐시 사용량을 줄여 성능에 유리할 수 있음
  • 컴파일러는 가능한 경우 이런 짧은 인코딩을 활용할 수 있음

프리픽스와 15바이트 명령 길이 제한

  • x86 명령은 동작을 바꾸는 프리픽스 바이트를 가질 수 있음
  • 64비트 코드에서 흔히 쓰이는 REX 프리픽스는 32비트 코드보다 더 넓은 레지스터 범위에 접근하는 데 사용됨
  • CPU는 효과가 없는 REX 프리픽스도 받아들임
    • 4004cc는 8비트 add al,0CCh 앞에 REX 바이트가 붙은 형태지만, 이 경우 REX는 아무 효과가 없음
    • REX 프리픽스를 두 개 붙여도 CPU는 실행할 수 있으며, WinDbg를 포함한 여러 디스어셈블러는 이를 혼동할 수 있음
  • x86 호환 CPU에서 현재 명령 길이는 15바이트가 하드 리밋임
    • 15바이트를 넘는 명령은 유효하지 않은 명령으로 간주되어 예외를 발생시킴
    • 오래된 CPU에는 프리픽스에 대한 다른 제한도 있고, LOCK 프리픽스처럼 사용할 수 있는 조건이 더 엄격한 프리픽스도 있음

주소 크기와 모드에 따라 달라지는 해석

  • Address override 프리픽스는 64비트 모드에서 32비트 주소를 참조하도록 만들 수 있음
    • 488d0424lea rax,[rsp]
    • 67488d04240x67 프리픽스 때문에 lea rax,[esp]
  • 32비트 코드에서는 Address override가 주소 모드를 16비트 주소로 바꿈
  • 같은 바이트열도 코드 세그먼트의 기본 피연산자 크기와 주소 크기를 알아야 올바르게 디스어셈블하거나 해석할 수 있음
    • 32비트 모드의 8b0424mov eax,dword ptr [esp]
    • 64비트 모드의 8b0424mov eax,dword ptr [rsp]
  • x86에서 INC regDEC reg에 쓰이던 40~4F 범위는 x64에서 REX prefix bytes로 쓰임
    • 32비트 모드에서 48 03 04 24dec eaxadd eax,dword ptr [esp] 두 명령으로 해석됨
    • 64비트 모드에서는 48030424add rax,qword ptr [rsp] 한 명령으로 해석됨
  • AMD64 설계자는 64비트 모드에서 레지스터 집합을 확장하기 위해 INC/DEC의 넓은 인코딩 공간을 새 프리픽스에 사용했으며, 해당 명령에는 이미 레지스터와 메모리를 모두 지원하는 다른 인코딩이 있었음

WinDbg와 INC reg의 64비트 모드 함정

  • WinDbg는 명령을 항상 32비트 모드처럼 어셈블하기 때문에, 64비트 코드에서 INC reg를 어셈블하려 하면 의도와 다른 결과가 생길 수 있음
  • 예시에서 inc eax는 실제 증가 명령이 아니라 다음 명령을 수식하는 쓸모없는 REX 프리픽스로 바뀜
  • 결과적으로 해당 바이트열은 inc가 아니라 jmp 명령 앞의 프리픽스로 해석됨

플래그 동작의 예외들

  • INC EAXADD EAX, 1과 비슷해 보이지만 완전히 같지 않음
    • ADD는 carry flag를 갱신함
    • INCcarry flag를 갱신하지 않음
  • TTD 에뮬레이터 구현 중 이 차이를 처음에 잘못 구현했고, 단위 테스트로 잡아냄
  • 대부분의 산술·논리 연산은 overflow, sign, zero, auxiliary carry, parity, carry flag를 설정함
  • CMPXCHG도 이런 플래그를 설정하지만, CMPXCHG8BCMPXCHG16Bzero flag만 수정함
  • 일부 명령은 플래그 일부를 정의되지 않은 상태로 남김
    • shift와 rotate 명령은 shift amount가 1보다 크면 overflow flag를 정의되지 않은 상태로 둠
    • 정의되지 않은 플래그의 실제 동작은 shift 연산의 내부 구현과 관련되어 있으며, 아키텍처마다 다를 수 있음
    • Atom 계열 CPU가 ALU에서 더 저렴하고 느린 방식으로 bit shift를 수행해 정의되지 않은 플래그 값이 달라진다는 이야기가 있지만, 직접 테스트하지는 않았음

shift 명령의 count 마스킹

  • 66c1e810shr ax,10h이며, AX를 16비트 오른쪽으로 shift함
    • AX는 16비트 레지스터이므로 결과는 0이 됨
  • c1e820shr eax,20h이며, 표면적으로는 EAX를 32비트 오른쪽으로 shift하는 명령처럼 보임
  • 실제로는 EAX 값이 변하지 않음
    • Intel SDM 기준으로 count는 1Fh와 마스크되어 rotation의 하위 5비트만 사용함
    • REX.W 프리픽스를 쓰면 마스크가 3Fh가 되어 최대 shift 값은 63비트가 됨
  • Microsoft 면접에서 “32비트 레지스터를 단일 명령으로 clear하는 모든 방법”을 묻는 질문 중 이 동작이 실제로 문제 된 적이 있음
    • 면접관은 shift를 쓰면 가능하다고 생각했지만, 32비트 레지스터에는 불가능하다고 답함

32비트와 64비트 코드에서 살아 있는 세그먼트

  • 세그먼트 메모리는 16비트 코드의 유물처럼 보일 수 있지만, 32비트와 64비트 코드에서도 실제 효과가 있음
  • 대부분의 OS는 거의 flat한 메모리 모델을 사용하고 세그먼트 base address를 0으로 두기 때문에 평소에는 잘 의식하지 않음
    • 64비트 모드에서 CPU는 CS, DS, ES, SS의 세그먼트 base를 항상 0으로 취급함
  • 예외적으로 thread local storage에는 FSGS 같은 extra segment register가 쓰임
  • 정정 사항으로, FS/GS 세그먼트의 base는 비권한 코드에서도 rdfsbase, wrfsbase, rdgsbase, wrgsbase 명령으로 읽을 수 있음
    • 이 명령들은 Ivy Bridge, 즉 2012년부터 사용 가능함

Windows TEB 접근과 FS/GS

  • Windows에서 FSGS는 TEB(Thread Execution Block)를 참조하는 데 쓰임
  • TEB 구조체는 구조체 시작 지점의 flat address를 가리키는 self 포인터를 갖고 있으며, 이 주소는 해당 세그먼트의 base이기도 함
  • 32비트 프로세스에서는 TEB가 FS로 위치함
    • GetLastErrorfs:[00000018h]에서 TEB.NtTib.Self를 가져온 뒤, [eax+34h]에서 LastErrorValue를 읽음
  • 64비트 프로세스에서는 TEB가 GS로 위치함
    • GetLastErrorgs:[30h]에서 포인터를 읽고, [rax+68h]에서 값을 가져옴
  • 64비트 OS에서 실행되는 32비트 프로세스는 32비트 TEB와 64비트 TEB를 모두 가지며, 32비트 프로세스 안에서 실행되는 64비트 WOW 코드처럼 두 TEB에 접근해야 유용한 맥락이 있음

세그먼트 base를 결정하는 방식도 모드마다 다름

  • FSGS의 base address를 결정하는 CPU 설정은 32비트 모드와 64비트 모드에서 다름
  • 32비트 모드에서는 세그먼트 레지스터의 실제 값이 Global Descriptor TableLocal Descriptor Table에 정의된 segment descriptor를 참조함
  • 64비트 모드에서는 base가 두 MSR로 제어됨
    • FS Base, Intel SDM의 IA32_FS_BASE
    • GS Base, Intel SDM의 IA32_GS_BASE
  • 이 구조 때문에 64비트 모드에서는 FSGS의 실제 레지스터 값 자체가 중요하지 않음
    • 중요한 것은 세그먼트 override 프리픽스임
  • WinDbg에서 32비트 프로세스 디버깅 중에는 FS 레지스터 값을 사용해 “FS segment” 내용을 dump할 수 있음
  • 64비트 프로세스에서는 같은 방식이 동작하지 않으며, 세그먼트 값보다 세그먼트 override 프리픽스가 의미를 가짐

에뮬레이터 구현자가 얻는 실무적 교훈

  • x86 에뮬레이터를 만들면 명령 인코딩, 프리픽스, 플래그, shift count, 세그먼트 같은 CPU의 실제 동작을 세밀하게 다뤄야 함
  • 이런 규칙 중 상당수는 일반 코드 작성에는 거의 쓸모없지만, 에뮬레이터 구현에는 직접적인 요구사항이 됨
  • 시행착오와 멘토링을 통해 배운 내용이 많으며, 오래된 에뮬레이터 경험을 가진 Darek Mihocka와 emulators.com도 함께 소개됨
  • x86 최적화와 저수준 동작에 관심이 있다면 Agner Fog’s website의 자료가 유용함

댓글과 토론

Hacker News 의견들
  • 덤으로 BSF/BSR에도 특이점이 있음. Intel SDM은 입력이 0이면 목적지 값이 정의되지 않는다고 하지만, AMD는 그 경우 목적지가 수정되지 않는다고 문서화함
    그런데 glibc는 Intel에서도 목적지가 수정되지 않는다는 미문서화 사실을 그대로 사용함 [1]. 이 때문에 내 바이너리 변환기에서 문제 원인을 찾는 데 꽤 오래 걸렸음
    또 TZCNT/LZCNT는 F3 접두사가 붙은 BSF/BSR 인코딩인데, 이 확장을 지원하지 않는 구형 프로세서에서는 접두사가 조용히 무시됨. 그래서 같은 코드가 CPU에 따라 다르게 동작하지만, 적어도 이건 문서화되어 있음
    인코딩에선 사람들이 접두사를 자주 욕하지만, 개인적으로는 그게 최악은 아님. 잘 알려져 있고 어느 정도 문서화도 되어 있음. 더 나쁜 특이점도 있음. 예를 들어 REX/VEX/EVEX.RXB 확장 비트는 적용 대상이 아니면 무시되지만, 마스크 레지스터 k0-k7에서는 #UD를 일으킴. 그런데 레지스터가 ModRM.rm에 인코딩되면 또 확장 비트가 무시됨
    APX는 특이점의 수준을 한 단계 더 올림. REX2 접두사는 범용 레지스터 r16-r31은 인코딩할 수 있지만 xmm16-xmm31은 못 하고, EVEX 접두사는 opcode에 따라 레이아웃이 여럿이며, 레지스터에 쓰이는 확장 비트도 레지스터 종류에 따라 달라짐. XMM 레지스터는 X3:B3:rm과 V4:X3:idx를 쓰고, 범용 레지스터는 B4:B3:rm과 X4:X3:idx를 씀. 1년이 지났는데도 아직 APX 디코더를 끝내지 못해서 전체 목록은 못 주겠음
    [1]: https://sourceware.org/bugzilla/show_bug.cgi?id=31748

    • 지난 1년 동안 틈틈이 QEMU의 x86 디코더를 다시 작성하고 있음. 처음엔 AVX 지원을 넣기 위해 필요한 작업이었지만, 이제는 다시 작성할 opcode가 몇 개만 남았고 그 뒤에는 APX 지원도 크게 어렵지 않을 것 같음
      EVEX는 opcode를 읽고 EVEX 클래스를 식별할 때까지 원시 비트를 유지하는 방식으로 가려 함. 즉 즉시값 앞, 아마 ModRM 앞 단계까지 보존하는 계획임
      내 디코더는 대체로 매뉴얼의 표에 기반하고 있고, 코드는 대체로 괜찮음. 들여쓰기가 과하지 않고 단계도 대부분 분리하거나 식별하기 쉬움. 출력이 JIT 코드라서 아주 효율적일 필요는 없고 읽기 쉬운 쪽이 괜찮음. 대부분의 시간이 쓰이는 곳도 아님
      그래도 매뉴얼이 틀렸거나 전체 내용을 말하지 않는 경우가 여럿 있음. 표도 몇 년째 갱신되지 않았고, 예를 들어 K 레지스터 명령어도 없음. 앞으로는 수작업이 더 많아질 듯함
      맨 위 주석에 상황이 조금 설명되어 있음: https://github.com/qemu/qemu/blob/59084feb256c617063e0dbe7e6...
      위에서 말했듯 아직 예전 코드가 처리하는 명령어가 몇 개 남아 있고, 특히 BT/BTS/BTR/BTC가 그렇다. 코드는 작성했지만 아직 병합하지 않았음
    • 486과 Pentium 시절의 또 다른 특이점이 있음. BSWAP EAX는 리틀 엔디언과 빅 엔디언을 서로 변환하고, 처음부터 32비트 명령어였음
      그런데 16비트와 32비트 모드를 전환하는 0x66 접두사가 있음. 이걸 BSWAP EAX에 적용하면 정의되지 않은 이상한 일이 벌어짐
      어떤 CPU 아키텍처에서는 Intel과 AMD 차이처럼 접두사가 그냥 무시됐고, 다른 곳에서는 내가 “내부 스왑”이라고 부르는 동작을 했음. 예를 들어 EAX에 저장된 네 바이트 중 1번과 2번 바이트가 서로 바뀜
      0x11223344가 0x11332244가 됨
    • 이 모든 로직을 실리콘에서 정확하게, 게다가 빠르게 동작하게 만들어야 한다고 생각해보면 아찔함
      예전엔 x86이 Intel의 해자였지만, 이제는 짊어지고 가야 할 악몽 같은 짐처럼 보임
    • LZCNT의 의미론과 인코딩 조합은 자책골처럼 느껴짐. 레거시에서 무시되는 접두사가 붙은 BSR 명령어로 인코딩되는데, 0이 아닌 입력에서는 반환값이 피연산자 크기에서 레거시 버전의 반환값을 뺀 값임
      clz()라는 함수가 존재하긴 하지만, LZCNT가 그냥 0 입력 의미만 다른 BSR이었다면 호환성을 얻기 위해 구현에서 추가 뺄셈을 하는 정도는 작은 비용이었을 것 같음
    • 이 분야는 잘 모르지만, JTAG 인터페이스를 x86 CPU에 연결해서 명령어 단위로 실행하며 모든 레지스터 값을 기록하면 흥미로울 것 같음
      같은 프로그램을 에뮬레이트한 CPU에서도 실행하고 매 명령어마다 상태가 같은지 검증하면, 에뮬레이터가 하드웨어를 완벽히 모사하는지 테스트할 수 있음
  • 멋진 사람임. 어셈블리 작성은 단순하게 느껴지고, 세로로 늘어선 미학도 좋아함
    OP 같은 작업에 조금이라도 가까웠던 경험은 JS 하는 친구에게 스택을 이해시키려다가 작은 ISA를 가진 미니 VM을 같이 만든 정도였음: https://gist.github.com/darighost/2d880fe27510e0c90f75680bfe...
    더 깊게 파고들 수도 있었고 나도 그랬으면 좋았겠지만, 그러면 원래 교육 목적에서 벗어났을 것 같음. 그 친구에게 아직 같이 공부하고 싶은지 연락해봐야겠음. 친구는 멋진 웹 개발로 돈을 많이 벌어서 깊이 파고들 시간이 없고, 나는 실업자라 시간과 에너지가 거의 무한한 바다 같아서 쉽지 않음

    • 그걸 계기로 친구에게 JS를 배워보는 것도 좋을 듯함
    • 비전공자로서 이 코드가 내부 동작을 이해하기 시작하는 좋은 입구가 됐고, 덕분에 꽤 멋지고 어려운 어셈블리 여정으로 이어졌음. 더 깊이 파볼 생각임
  • Justine Tunney와 그 에뮬레이터도 볼 만함. https://justine.lol/blinkenlights/
    문서는 CPU가 어떻게 동작하는지 훌륭하게 둘러보게 해줌

    • 놀라움. 매번 감탄하게 만듦
    • Tunney라는 이름은 2014년쯤 노숙하며 떠돌고 Occupy 관련 트위터 헛소리를 하던 시절로 기억남
  • 이전 논의는 여기 있음: https://news.ycombinator.com/item?id=34636699
    벌써 16개월이 지났다니 믿기지 않음. 시간이 정말 빠름

  • “CPU 에뮬레이터를 작성하는 것이 CPU 동작을 진짜 이해하는 최고의 방법”이라는 데는 강하게 동의하지 않음
    가장 좋은 방법은 괜찮은 컴퓨터과학 수업에서 하듯이 게이트 수준에서 CPU를 만드는 것임. 축소된 ARM을 처음부터 만든 작업이 정말 재미있었음

    • 둘 다 유용하다고 봄. 다만 현대 CPU를 게이트 수준에서 설계하는 건 대부분에게 손이 닿지 않고, 대학에서 설계하는 CPU와 실제 코드를 실행하는 CPU 사이에는 큰 간극이 있음
      현대 CPU 에뮬레이터를 만드는 쪽이 좀 더 접근 가능한 도전이고, 일부만 동작해도 여전히 교육 효과가 큼
    • 동의함. 마이크로코드, 파이프라인, 슈퍼스칼라, 분기 예측, L1 데이터/명령 캐시, write-back L2 캐시 컨트롤러를 갖춘 기본 프로세서도 만만치 않음
      대부분의 소프트웨어 엔지니어는 데이터 해저드, 캐시 무효화, 파이프라인 정지에 대한 이해가 불완전함
    • 둘 다 맞다고 봄. 74xx 칩을 엮는 건 엄청 만족스럽고, 전기적 측면과 내부 절충을 감으로 익힐 수 있음
      하지만 의미 있는 작업에 쓰고 싶은 CPU를 그렇게 만들기 시작하면 그 세부사항에는 흥미가 떨어지기 시작함. 그때는 동작과 명세의 복잡성이 더 흥미로워지고, 에뮬레이터 접근이 더 다루기 쉽고 더 많은 동작 유형을 다룰 수 있음
    • Nand2Tetris를 따라가며 게이트 수준에서 쌓아 올리는 과정을 진행 중이고, 방금 VM 에뮬레이터 장을 끝냈는데 정말 오래 걸렸음. 이제 컴파일 단계로 넘어감
    • 반대로, 게이트 수준 CPU에서 정말 메모리 세그먼테이션까지 구현할 건가? 실제로 동작하는 CPU를 만든 다음, 결함까지 포함해 실제 CPU를 에뮬레이트하는 두 단계가 모두 진짜 이해에 필요하다고 봄
  • 장난감이 아닌 아키텍처 12개 정도의 빠른 에뮬레이터를 작성했고, 몇몇 JIT 변환기도 만들었지만 x86은 아직도 PTSD를 줌. 이렇게 지저분한 아키텍처는 본 적이 없음. 역사도 있고 이유도 있지만, 그래도 참 심함

    • x86 아키텍처를 공부하는 건 불규칙성과 흔적 기관이 많고 경쟁하는 문법 체계가 있는 언어, 예를 들면 프랑스어를 공부하는 느낌임. RISC-V나 ARMv8 같은 다른 아키텍처는 훨씬 일관적임
    • “이보다 더 지저분한 아키텍처를 본 적이 없다”면 Itanium이 있음. 매뉴얼을 열 때마다 일부러 찾지 않아도 “대체 무슨 생각이었지?” 싶은 걸 새로 발견함
    • 공감함. 아마 처음부터 그걸로 시작했어야 했을지도 모름
  • 최근 사이드 프로젝트로 x86-64 디코더의 상당 부분을 구현했는데 [1], 요즘 들어 더 복잡해졌다는 점에 꽤 놀랐음. 내 목적에는 Sandpile.org [2]가 정말 유용했음
    [1] 정확히는 아직 공개되지 않은 또 다른 사이드 프로젝트를 위해 만든 Fabian Giesen의 disfilter x86-64 버전: https://gist.github.com/lifthrasiir/df47509caac2f065032ef72e...
    [2] https://sandpile.org/

  • 대학에서 만든 68k 디스어셈블러는 Neo가 “쿵푸를 알아요”라고 하는 순간 같았음. 고급 언어에서 트랜지스터까지, 다시 거꾸로 코드에 대해 추론하게 해준 빠진 연결고리였음
    전체 에뮬레이터를 작성하면 그보다 한 자릿수는 더 효과적일 것 같음. 좋은 글임

    • ISA 에뮬레이터 작성은 현대 슈퍼스칼라 CPU가 어떻게 동작하는지 이해하는 데는 사실 별 도움이 안 된다고 봄. 거의 모든 부분이 내부에 숨겨진 최적화이기 때문임
  • 내 기억이 틀렸던 듯함. salsa20 변형과 기계어가 원래 cryp.to에 있었다고 기억했는데, Dan Bernstein의 사이트는 https://cr.yp.to/였음
    스타트업에서 저장 데이터 암호화, 스트리밍 암호화 등을 검토하던 시절에 Dan의 페이지에는 대상 칩셋과 명령어 집합별로 구현이 여럿 있었음. 그의 어셈블러 표현에서 교차 컴파일된 것들이었음
    VM을 써보며 2000년대 초중반 당시 어떤 명령어 집합이 지원되는지 보는 게 흥미로웠음. 테스트 중에는 VM이 지원한다고 주장했지만 구현이 완전히 지원되지 않아 가끔 문제가 생기기도 했음

    • Dan Berstain의 사이트를 말하는 거겠지… 잠깐
  • 여기서 RISC와 비교해 x86 어셈블리가 얼마나 고통스럽다는 얘기가 많은 게 재미있음. 나는 코드를 다시 객체 파일로 분리할 때 정반대 문제가 있음
    이 용도에서는 x86 분석이 정말 쉽고, MIPS는 악몽 같았음. 주로 신경 쓰는 게 코드와 데이터에 대한 참조이기 때문임. x86에는 포인터 크기 즉시 상수가 있고, MIPS에는 HI16/LO16 재배치 쌍이 있어서 레지스터 사용 그래프, 코드 흐름, 분기 지연 명령어와 얽히며 온갖 문제가 생김
    그렇다고 내가 x86을 칭찬하는 건 아님

    • 맞음. x86은 이상하지만 가변 길이 명령어는 텍스트 형태로 풀어놓고 나면 사실 보기 좋고 이해하기도 쉬움. 문제는 명령어 중간에 다른 명령어를 숨길 수 있어서 안전하지 않다는 데 있음
      x86 어셈블리와 C를 비교하며 배우는 가장 큰 점은 부호 있음/없음이 타입이 아니라 연산의 속성이 된다는 것임
      플래그를 활용할 수 있으면 좋겠고 PPC나 armv7 같은 일부 아키텍처에서는 더 쉽지만, x86은 플래그를 너무 쉽게 덮어써서 값을 활용하기가 너무 어려움