아래 코드를 실행하면 놀랍게도..
입력된 숫자에 대한 팩토리얼 값이 출력된다. 단, 플랫폼은 x86 한정으로..;;
(뭐, 컴퓨터가 돌아가는 원리를 아는 사람이라거나 맨날 기계어 코드 들여다보는 게 직업인 사람이라면 전혀 새삼스러운 일이 아니겠지만)

비주얼 C++뿐만이 아니라 도스용 DJGPP로도 프로그램이 잘 동작하는 걸 확인했다. 둘 다 IA32 아키텍처인 건 동일하니까 이는 당연한 귀결.

int main(int argc, char* argv[])
{
 BYTE instrs[]={
  0x55,
  0x8B,0xEC,
  0x83,0xEC,0x08,
  0xC7,0x45,0xFC,0x01,0x00,0x00,0x00,
  0x8B,0x45,0xFC,
  0x89,0x45,0xF8,
  0xEB,0x09,
  0x8B,0x4D,0xF8,
  0x83,0xC1,0x01,
  0x89,0x4D,0xF8,
  0x8B,0x55,0xF8,
  0x3B,0x55,0x08,
  0x7F,0x0C,
  0x8B,0x45,0xFC,
  0x0F,0xAF,0x45,0xF8,
  0x89,0x45,0xFC,
  0xEB,0xE3,
  0x8B,0x45,0xFC,
  0x8B,0xE5,
  0x5D,
  0xC3
 };

 PVOID pv = instrs;
 
int (*pfn)(int) = reinterpret_cast<int (*)(int)>(pv), y;
 while(1) {
  printf("? "); scanf("%d", &y); if(y<1) break;
  printf("%d\n", pfn(y));
 }
 return 0;
}


프로그래밍 언어의 인터프리터 내지 just-in-time 컴파일러를 만든다거나,
가상 기계 에뮬레이터를 만드는 것은 어렵지 않다.
결국 핵심이 뭐냐 하면 컴퓨터가 직통으로 실행하는 코드 자체를 실행 시간에 메모리에다 생성하는 건데,
함수 포인터가 가리키고 있는 게 바로 저런 것들이다.

물론, 위에서처럼 실행해야 할 코드가 완전히 고정돼 있는 경우라면
소스 코드에다 인라인 어셈블리를 집어넣으면 되겠지만, 그 코드는 데이터 영역에 들어가는 게 아니라 소스 코드(text) 영역에 그대로 포함되어 버리게 될 것이다.

위의 팩토리얼 함수는 물론 컴파일러가 생성해 준 코드를 복사한 것이다.
최적화를 안 한지라, 간단한 for 루프 하나밖에 없는 함수가 코드 길이가 꽤 길다.
최적화를 하고 나면 정상적인 함수 입출력 껍데기에 해당하는 코드조차도 거추장스러운지 생성되지 않아서
그걸 저렇게 따로 떼어내서 쓸 수가 없었다.

Posted by 사무엘

2011/07/08 08:06 2011/07/08 08:06
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/537

Trackback URL : http://moogi.new21.org/tc/trackback/537

Comments List

  1. 남정현 2011/07/08 10:46 # M/D Reply Permalink

    포인터의 미학이 느껴지는 코드네요. 문득 궁금한게 있는데, 혹시 위에 있는 코드는 DEP (Data Execution Prevention)가 Windows에서 켜져있더라도 잘 작동하는 코드인가요?

    1. 사무엘 2011/07/08 13:20 # M/D Permalink

      아, DEP를 깜빡했어요.
      사실은 이 블로그 글을 작성하면서도... '코드를 동적으로 생성하려면 원래는 별도의 가상 메모리 영역에다 넣고 VirtualProtect로 PAGE_EXECUTE 권한이라도 줘야 하지 않나? 어째 스택 메모리에다 저렇게 바로 넣어도 잘 돌아가네?' 이상하게 생각했습니다. 생각해 보니 그게 꺼져 있어서 그렇군요.
      DEP가 적용되어 있다면 저 코드는 당연히 바로 access violation이 납니다. ㅎㅎ

    2. 남정현 2011/07/08 13:36 # M/D Permalink

      그렇군요. 사실 DEP가 있다는걸 알았지만서도 구체적으로 DEP가 어떤 상황에서 작동하는지 궁금한 점이 있었는데 이런 상황을 두고 말하는 것이었군요.

Leave a comment

동일한 기능을 하는 프로그램이 여러 CPU 아키텍처로 포팅된 실행 파일을 살펴보면...
우리에게 아주 친숙한 x86 아키텍처용 EXE는 크기가 가장 작다고 단정적으로 말할 수는 없지만 상당히 작은 편이다.
이보다 코드 사이즈가 더 작게 컴파일되는 아키텍처는 본인이 보기엔 찾기가 어렵다.
EXE 파일의 기계어 코드 부분을 헥사 에디터로 들여다봐도 코드 바이트가 좀 조밀조밀 있는 편이다. 그 반면 IA64나 MIPS 같은 아키텍처 EXE의 기계어 코드를 들여다보면, 4~8바이트 단위로 패턴이 느껴진다.

물론 인텔 아키텍처도 그나마 32비트와 64비트로 가면서 인스트럭션의 평균적인 크기가 길어져 오긴 했다. 그런데 초기의 16비트 명령 체계는 정말 아담했으며, 어셈블리 튜닝을 쓰면 오늘날의 컴퓨터 아키텍처로는 상상도 할 수 없는 작은 크기의 프로그램으로 온갖 큼직한 기능을 넣을 수 있었다. ^^;;
인텔 아키텍처는 하위 호환성을 최대한 존중하고 있으니, 옛날에 정한 1바이트짜리 코드 바이트가 다 선점되었으면 32비트나 64비트 때 추가된 명령은 탈출문자 접두사를 붙여서 5바이트~6바이트... 이런 식으로... 근본적으로 길어질 수밖에 없는 셈이다.

컴퓨터 아키텍처에 대해서 배운 전산 전공자라면, CISC 방식과 RISC 방식의 차이에 대해서는 익히 들어 봤을 것이며, 오늘날엔 이런 식의 단편적인 구분이 별 의미가 없어졌다는 것까지도 알고 있을 것이다.

옛날은 메모리가 아주 귀한 자원이던 시절이었다. 그래서 인텔 x86은 코드를 적재하는 데 필요한 기억장소의 크기를 최대한 아끼는 방향으로 설계되었다. 기계어 코드의 크기가 1바이트부터 시작해 아주 가변적이고, 각 명령 하나하나에 꽤 함축적으로 많은 뜻을 포함시킬 수 있다.

많은 뜻이라 함은, 명령을 내릴 때 상대 주소라든가 절대 주소라든가 실제 상수 같은 개념을 한 인스트럭션에다가 바로 표현할 수 있음을 의미한다. 다른 아키텍처라면 임시값을 또 레지스터에다 먼저 저장하고 전처리를 해서 여러 인스트럭션을 거쳐 표현해야 할 명령을, 한 인스트럭션만으로 표현할 수 있다는 뜻이다. 이것을 CISC 방식이라고 하며, 앞의 C는 complex를 뜻한다. 인텔 x86은 CISC 방식 아키텍처의 대표적인 예이다.

CISC 방식는 상술했듯이 코드 길이가 짧아진다는 장점이 있지만 이를 하드웨어적으로 해석하는 회로가 필연적으로 복잡해지고 만들기 어려워진다는 단점이 있다. 이는 전력 소모의 증가로까지 이어진다. CPU에서 실제로 명령을 실행하는 부분이 아니라 이게 무슨 명령인지 파악하는 부분부터가 오버헤드가 커진다는 뜻이다.

그래서 CISC 방식은 근본적으로 임베디드나 모바일 환경에는 적합하지 않다. 이런 이유로 인해, 오늘날 전세계적인 각광을 받고 있는 스마트폰에서는 ARM이라는 RISC (Reduced) 방식 아키텍처가 쓰이고 있다. ARM이 스마트폰 세계에서 거의 인텔 x86 같은 본좌 인지도를 차지한 것이다.

RISC는 설계 철학이 CISC와는 반대이다. 인스트럭션의 크기는 기계가 한 번에 인식하는 machine word 크기와 일대일 대응하거나 최소한 배수 단위이다. 인스트럭션을 fetch, decode하고 의미를 파악하는 절차가 아주 간편하다. 최소 의미 단위가 작은 대신에 임시 작업용으로 범용 레지스터를 CISC 방식 CPU보다 꽤 많이 준다.

이런 CPU는 심지어 구조체 멤버를 인식하는 단위에도 제약이 있다. 포인터의 오프셋이 machine word의 배수 단위로 딱 떨어지지 않고 사이에 걸쳐 있는데 그 주소를 참조시키면 CPU가 에러를 일으킨다. 성능과 효율을 위해, 이런 복잡한 상황은 과감하게 처리를 거부하는 방법을 택한 것이다.

인텔 x86은 그렇지 않다. 명령어의 실행부터가 워낙 지저분한 바이트 단위 fetching에 익숙한지라, 오프셋이 저런 경우에도 한 사이클만에 참조를 못 하더라도 여러 사이클로 나눠서 사용자가 원하는 데이터를 얻어 준다.

RISC 아키텍처에서 돌아가는 실행 파일을 들여다보면 기계어 코드가 진짜로 4바이트나 8바이트 패턴으로 쫘르륵 나열되어 있는 걸 볼 수 있다. 그리고 동일한 코드를 컴파일한 실행 파일 크기가 x86 같은 아키텍처의 것보다 더 크고, 실행 파일의 압축률도 더욱 높다(듬성듬성하다는 뜻). 다만 한 기능을 수행하는 데 드는 CPU 명령 수가 증가하고 그럴 수록 side effect라든가 복잡도도 더 증가하는 만큼, RISC 아키텍처 코드를 컴파일러가 최적화하기는 더욱 어려울 것이다.

이렇듯, 컴퓨터 아키텍처에서 CISC냐 RISC냐 하는 케케묵은 논쟁은 자동차로 치면 전륜 구동이냐 후륜 구동이냐, 철도 차량으로 치면 동력 분산식이냐 동력 집중식이냐 하는 차원의 재미있는 주제이다. 요즘은 x86 같은 CISC 방식 CPU도 내부적으로는 복잡하고 함축적인 명령을 다시 RISC 식 명령으로 나눠서 파이프라인을 수행함으로써 RISC의 장점을 취하려고 하고 있다.

그리고 문득 든 생각:
어느 기계에서나 이식 가능한 평범한 C/C++ 코드는 자연어로 치면 "나는 철수입니다" 같은 평이한 문장에다 대응시킬 수 있다.
그렇다면 도저히 포팅이 불가능한 인라인 어셈블리 코드는, 자연어로 치면 특정 언어의 음운 특성이나 특정 문화 배경에 대한 이해 없이는 도저히 이해할 수도, 문자 그대로 번역할 수도 없는 함축적인 유머나 언어 유희에다 비유할 수 있겠다.

Posted by 사무엘

2010/09/20 09:16 2010/09/20 09:16
, , , ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/376

Trackback URL : http://moogi.new21.org/tc/trackback/376

Comments List

  1. 김재주 2010/09/20 09:47 # M/D Reply Permalink

    RISC 방식은 컴파일러가 코드를 최적화하긴 어렵지만 반대로 CPU에서 oooe 같은 거 할 때는 편리하죠. 파이프라이닝도 한결 간단히 할 수 있고요.

    뭐 요새는 CISC 아키텍쳐에서도 각각의 인스트럭션을 마이크로 인스트럭션으로 분해해서 실행하기 때문에 그리 큰 장점이라고 보긴 어렵지만요

    1. 사무엘 2010/09/20 22:31 # M/D Permalink

      앞으로 뭔가 새로운 인스트럭션이 추가될 일이 절대 없다면야 거기에 딱 맞춰진 RISC 방식이 여러 모로 유리하겠습니다만, 하위 호환성 존중하는 한편으로 탈출 문자로 새로운 명령어를 추가하고 또 추가하는 x86스러운 방식이라면... 융통성이 뛰어난 CISC 방식 말고 대안이 없을 것 같긴 합니다.
      언제적 일이더라? CPU가 big endian 방식인 컴퓨터를 보고는 깜놀 했던 기억이 납니다. 마치 여행을 많이 다니면서 세상 견문을 넓힐 필요가 있듯이 다양한 기계에서 프로그래밍을 해 볼 필요는 있는 것 같아요.

  2. 김 기윤 2010/09/20 23:05 # M/D Reply Permalink

    컴퓨터 구조 시간에 수업했던 내용이 생각나네요- (..)

    다만 그렇게까지 low-level 로 분석하거나 할 일은 없으므로 이해는 가지만, 실감은 안날달까..? 그런 느낌입니다

    1. 사무엘 2010/09/21 10:10 # M/D Permalink

      뭐, 꼭 알 필요는 없지만 알아 두면 좋은 내용이죠.
      저는 하드웨어나 시스템 프로그래밍 쪽에 그렇게 관심이 많은 게 아니지만,
      C/C++ 자체가 워낙 네이티브 코드 지향적이고 돌아가는 컴퓨터 환경의 특성을 잘 나타내는 언어이다 보니, 이런 감각도 어느 샌가 자연스럽게 터득을 하게 되더라고요.
      물론 그런 면모 때문에 C/C++이 비판을 받기도 합니다만.

  3. 김재주 2010/09/21 15:02 # M/D Reply Permalink

    제가 항상 하는 생각인데

    IT업계 종사자의 평균수명을 깎아먹는 요인은 몇 가지가 있습니다.

    1. 야근
    2. 술 (회식)
    3. 담배
    4. C/C++
    ...

    1. 사무엘 2010/09/21 20:33 # M/D Permalink

      남이 개념 없이 짜 놓은 C/C++ 코드를 디버그해야 하거나 memory leak을 찾아내야 한다면 정말로 수명이 단축될 수도 있겠습니다. -_-

Leave a comment
윈도우 환경에서야, 실행 가능한 기계어 코드가 들어있는 다른 모듈을 동적으로 로드하는 것이 일도 아니다. LoadLibrary, GetProcAddress라는 마법 같은 API가 있기 때문이다. 플러그 인 같은 것도 프로토콜만 하나 잘 짜 놓으면 얼마든지 만들 수 있다.

하지만 도스 시절에 그런 일종의 DLL이라는 것을 직접 구현을 어떻게 했을까?
본인은 도스 환경에서 하드웨어를 직접 제어한다거나 시스템 프로그래밍 경험이 전혀 없다. PC 통신 시절에 올라오던 랄프 브라운의 "인터럽트 릴리즈" 이런 것들도 내게는 완전 외계인 문서였다.
32비트 윈도우에서 곧바로 C/C++을 공부한 경우이다 보니, 지금 다시 생각해 보면 그게 무척 신기하다. 그래서 점점 다시 저수준 시스템 쪽으로 회귀 중이다.

도스용 아래아한글은 2.5에서 덧실행이라는 기능이 추가되었다. 그래서 과거 1.2 시절에 잠깐 있었던 테트리스 게임도 덧실행으로 부활하고, 아래아한글 안에서 숫제 한네트라는 통신 에뮬레이터까지 구동할 수 있었다. (개인적으로 그래픽 에디터를 그렇게 덧실행으로 내장했으면 무척 좋았을 것 같다.)

3.0에서는 덧실행의 활용의 폭이 더욱 커져서 계산기, 지뢰 찾기, CD 플레이어도 제공했으며 심지어 화면 보호기까지 덧실행 프로그램으로 독립했다. 2.5 확장팩에서부터 제공되던 영한 사전도 GUI는 덧실행 애플릿 형태였다. 이런 덧실행 프로그램들은 내부적으로 32비트 네이티브 코드였으며, 응당 32비트 에디션(HWP386)에서만 지원되었다.

도스용 이야기 역시 이름은 덧실행이 아니지만, 이런 덧실행에 해당하는 여러 기능이 있었다. 그림 파일 뷰어도 있고, AI가 상당히 똑똑했던 걸로 기억하는 오목 게임도 있었다. 16컬러에서밖에 실행되지 않았던 아래아한글과는 달리 이야기는 256색/트루컬러 그래픽까지 지원했지만, 내부 코드는 여전히 볼랜드 C++로 빌드된 16비트 프로그램이었으며, 덧실행 역시 16비트 코드라는 게 아래아한글과는 달랐다.

물론 덧실행에 앞서 이들 프로그램은 그래픽/프린터 드라이버가 이미 일종의 DLL이며 아래아한글의 경우 폰트 드라이버라는 계층이 있었지만, 이들보다 화면에 보이는 GUI가 있는 덧실행 프로그램이 사용자에게 더욱 존재감 있게 다가온 것 역시 부인할 수 없는 사실이다.

이런 덧실행들은 어떻게 만들었을까? 아래아한글 덧실행의 경우, SDK가 있었다.
아래아한글 32비트 에디션과 덧실행 프로그램들은 왓콤 C/C++로 빌드되었는데, SDK가 지정한 각종 함수/구조체 프로토콜에 맞게 덧실행 프로그램을 작성한 후, 역시 SDK가 제공하는 특수한 라이브러리를 써서 링크하고 바이너리에다 고유한 post processing을 마치면 간단히 덧실행 프로그램이 만들어졌다. 얘네들은 헤더만 다를 뿐, 내부적으로는 인텔 32비트 어셈블리 기계어 코드가 들어있는 exe 그대로였다.

아래아한글은 자기 밑에 실행되는 덧실행 프로그램에다가 지금 돌아가는 비디오 환경 같은 걸 알려 주고, 하드웨어 독립적인 각종 그래픽 루틴, 그리고 요즘 GUI 운영체제들이 다 그렇듯이 막강한 한글 입출력 엔진을 이용하여 글자를 찍고 GUI를 출력하는 API를 덧실행에다가 제공해 주었다.

그때 화면 보호기 중에는 아주 고전적인 "우주 여행"도 있었고 점 찍기, 선 그리기, 생명 게임, 퍼즐, 벌떼 같은 것들이 기본 제공되었다. 우주 여행은 단순히 전진만 하는 게 아니라 방향을 꺾는 것도 있고 무척 박진감 넘치고 원근감이 무척 멋지게 잘 표현돼 있었다.

그리고 벌떼도 나름대로 2차원 공간에서의 boid 시뮬레이션인데 움직임이 굉장히 사실적이어서 저런 걸 어떻게 짰을지가 무척 궁금했다.

이것 말고 선이나 점을 그리는 것은 while 루프 안에서 랜덤하게 화면에 낙서를 하는 녀석이었다. 특히 점 찍기는 그냥 화면 가득히 검은 1픽셀 점을 채워넣는 것으로, 프로그램 파일의 크기가 400바이트도 채 되지 않았다.

그래서 문득 궁금해졌다. 크기도 꽤 작고 프로그램을 어떻게 짰을지 감도 오고 하니,
코드 부분을 긁어 와서 디스어셈블을 해 봤다.

본인은 어셈블러 쪽 지식이 거의 없다. 뭐 복잡한 레지스터 이름만 나와도 머리가 지끈거린다.
본인보다 더 고수이신 분이 있으면 아래의 해설에 오류 교정이나 보충할 만한 설명 있으면 얼마든지 환영한다.

; 레지스터에 주어져 있는 어떤 값들을 스택에다 push함.
; 화면 보호기들은 다들 이런 명령으로 시작하더라.
0012FDF8 53               push        ebx
0012FDF9 51               push        ecx
0012FDFA 52               push        edx

; EAX 레지스터의 값을 0으로 초기화. x xor x는 언제나 0이므로, mov 0을 하는 것보다 코드 길이가 짧아서 좋다.
0012FDFB 31 C0            xor         eax,eax

; 뭔가 초기화 함수를 호출한다.
; 아래아한글 덧실행 SDK가 제공해 주는 라이브러리 함수에다 static link를 한 것이다.
0012FDFD FF 15 88 02 00 00 call        dword ptr ds:[288h]
0012FE03 FF 15 50 02 00 00 call        dword ptr ds:[250h]

; 고급 언어로 치면 while문의 시작인 듯. while(CanContinue()); 로 딱 번역 가능한 문장이다.
; 함수 리턴값을 테스트하여 조건이 만족하는 경우 뺑뺑이 바깥으로 빠져나간다. (12FE54)
; 화면 보호기의 종료 조건을 테스트하는 것이므로, 아마 키보드나 마우스 입력을 감지하는 함수일 것이다.
0012FE09 FF 15 14 02 00 00 call        dword ptr ds:[214h]
0012FE0F 85 C0            test        eax,eax
0012FE11 75 41            jne         0012FE54

; 뭔가 포인터 참조를 하는 듯하다. x = ptr->member 정도? 먼저 ptr의 위치를 임시로 EDX에다 저장 후,
0012FE13 8B 15 24 03 00 00 mov         edx,dword ptr ds:[324h]

; 함수 호출을 염두에 두고, 아마 EAX의 값인 0을 얹어 놓는 것 같다. 이렇게 EAX를 자주 써먹는 이유는 아까 xor EAX,EAX와 마찬가지로 명령어 길이가 짧기 때문.
0012FE19 50               push        eax

; EBX에다가 화면 세로 해상도를 저장하는 것 같다.
0012FE1A 8B 5A 14         mov         ebx,dword ptr [edx+14h]
; 아마도 이 0x24C 오프셋이 난수를 되돌리는 함수인 듯하다.
0012FE1D FF 15 4C 02 00 00 call        dword ptr ds:[24Ch]

; 정확한 의미 잘 모름.;;
0012FE23 89 C2            mov         edx,eax
0012FE25 43               inc         ebx

; 32비트 숫자를 31만치 오른쪽으로 비트 shift 한다는 것은 결국 그 숫자의 부호만 남기고 싹 없앤다는 뜻인데.. 특별한 의미는 모르겠다. 나눗셈 연산 전에 부호와 관련된 무슨 플래그 설정인 듯.
0012FE26 C1 FA 1F         sar         edx,1Fh

; 이제 난수가 화면 세로 해상도의 범위 안에 있도록, 난수 값을 화면 해상도로 나눈 나머지를 구한다.
; idiv 명령은 한번에 몫과 나머지를 모두 구하는데, 나머지의 값을 EDX에다 저장해 준다. 그렇기 때문에 EDX를 push하는 것을 알 수 있다.
; 아까 inc ebx 명령은, 화면 해상도에 0이 들어오더라도 나눗셈 에러가 나지 않게 안전 조치를 취한 것 같다.
0012FE29 F7 FB            idiv        eax,ebx
0012FE2B 52               push        edx

; 세로에 이어 가로 위치 argument를 얹을 준비를 한다. 다시 EDX에다가 324h 포인터 위치를 저장하고,
0012FE2C 8B 15 24 03 00 00 mov         edx,dword ptr ds:[324h]

; 아까 0x14가 화면의 세로 해상도이고 이거는 10h이니 가로 해상도? ㅋㅋ
0012FE32 8B 5A 10         mov         ebx,dword ptr [edx+10h]
0012FE35 FF 15 4C 02 00 00 call        dword ptr ds:[24Ch]

; 아까와 거의 동일하다. 이번엔 inc 명령은 들어가 있지 않다.
0012FE3B 89 C2            mov         edx,eax
0012FE3D C1 FA 1F         sar         edx,1Fh
0012FE40 F7 FB            idiv        eax,ebx
0012FE42 52               push        edx

; 이제는 324h 오프셋 자체를 push한다. 이 값은 역시 덧실행 프로그램이 호스트(아래아한글) 프로그램으로부터 받은 핸들 같다.
; 이게 제일 나중에 push된다는 말은, C++ 코드 상으로 제일 첫째 argumet라는 뜻이다. 즉,
; SetPixel(hDC, x, y, 0); 에서 hDC뻘 된다는 뜻이다. 0은 아까 EAX의 값으로 대체했던 점의 색깔 정도? (이 화면 보호기는 화면을 온통 검은 점으로 채운다)
0012FE43 FF 35 24 03 00 00 push        dword ptr ds:[324h]

; 점 찍는 함수 드디어 호출!
0012FE49 FF 15 E0 01 00 00 call        dword ptr ds:[1E0h]

; 함수 호출 후 argument를 얹었던 스택 위치를 재정리하는 작업. 16바이트를 더하는 것을 보면, 위의 함수가 총 4개의 인자를 받았다는 것을 알 수 있다.
0012FE4F 83 C4 10         add         esp,10h

; 다시 while의 시작으로 빠꾸
0012FE52 EB B5            jmp         0012FE09

; 루프 끗.
0012FE54 5A               pop         edx
0012FE55 59               pop         ecx
0012FE56 5B               pop         ebx
0012FE57 C3               ret

따라서 위의 코드에서 loop 부분을 슈도코드로 재구성하면 대략 이런 형태가 되겠다.

HANDLE hHWPHost;
while( CanContinue() )
        PutPixel( hHWPHost, rand() % hHWPHost->nScreenX, rand() % (hHWPHost->nScreenY+1), 0 );

저 간단하기 그지없는 코드를 C언어로 짜도, 컴파일을 하면 저 정도 수준의 기계어 코드로 번역된다는 것이 놀랍다.
역시 전산학의 총아는 컴파일러이다. 컴파일러 제작자가 존경스럽다.

프로그램 논리를 다 알고 있는 초간단 코드도 겨우 인텔 CPU 인스트럭션 세트 매뉴얼을 뒤지고,
간단히 내가 생각하는 코드를 VC++로 최적화 없이 컴파일하여 살펴보면서 낑낑대면서 디스어셈블을 해 봤는데,
이보다 훨씬 더 복잡한 기계어 프로그램은, 암호 같은 숫자와 명령 코드 속에서 본디 의미와 논리를 찾는다는 게 거의 불가능에 가깝다.

커다란 wav 파일 하나 던져 주고서, 이걸 재생하지 않고서 무슨 소리인지 알아 맞히는 거나 다름없다.
wav는 그래도 손실 압축이라도 되지, 기계어 exe는 같은 크기이더라도 정보량이 훨씬 더 많으며 손실 압축을 할 수가 없다.

점 찍기와는 달리, 선 그리기만 해도 코드 길이가 저것보다 더 길고, 특히 난수를 다섯 개나 요청한다.
x1, y1, x2, y2, 그리고 색깔 이렇게 다섯 개인 것을 어셈블리 코드 상으로 확인을 할 수 있었다.

비록 도스에서 돌아가는 일개 덧실행 프로그램이지만
돌아가는 기계 환경이 동일하고 똑같은 32비트 기계어이기 때문에 비주얼 C++의 디버깅 기능으로 이렇게 간단히 디스어셈블리 결과를 들여다볼 수 있다는 게 신기했다 (실행은 당연히 못 하지만).

단지 운영체제에 따라 EXE 파일의 포맷이 다르며, 입출력이라든가 운영체제가 제공하는 기능을 요청할 때, 도스 프로그램은 인터럽트에 의존하는 반면, 윈도우는 커널 영역에 자리잡은 함수 호출 기법을 사용하는 식의 차이가 존재하는 것이다.

Posted by 사무엘

2010/01/12 11:40 2010/01/12 11:40
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/118

Trackback URL : http://moogi.new21.org/tc/trackback/118

Comments List

  1. 김 기윤 2011/01/06 20:22 # M/D Reply Permalink

    16bit 디셈블 코드를 설명하라- 라는 중간고사를 치렀던 적이 있습니다. 그냥 함수 하나를 호출했을 뿐인데, 디셈블 코드는 지옥이더군요-_-; 정말 컴파일러 제작자가 존경스럽습니다..

    1. 사무엘 2011/01/07 09:58 # M/D Permalink

      전산학의 3대 근간은 운영체제, 컴파일러, DB 엔진. ㄲㄲ

Leave a comment

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2013/06   »
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30            

Site Stats

Total hits:
166186
Today:
110
Yesterday:
171