초심 프로젝트의 두번째 주제로 Computer Architecture 를 주제로 새롭게 알게된 것을 정리했다. 교재는 다음과 같다.
주교제 | 컴퓨터 구조 및 설계 |
명령어
내장 프로그램 컴퓨터의 두 가지 기본 원리는 숫자와 같은 형태의 명령어를 사용한다는 것과 변경 가능한 메모리에 프로그램을 저장한다는 것이다. 이 두 원리 때문에 컴퓨터 하나로 과학자는 과학자대로, 금융가는 금융가 대로 자기가 필요한 일을 처리할 수 있는 것이다. 명령어 집합의 선택은 프로그램 실행에 필요한 명령어 개수와 명령어 하나 실행하는 데 필요한 클럭 사이클 수, 그리고 클럭 속도 간의 미묘한 균형을 요하는 문제이다. 명령어 집합 설계자가 이런 미묘한 결정을 내릴 때 지침이 될 수 있는 설계 원칙이 네 가지 있다.
- '간단하기 위해서는 규칙적인 것이 좋다.' MIPS 명령어 집합의 특성 중 많은 부분이 규칙성을 염두에 두고 결정된 것이다. 예를 들면 모든 명령어의 길이를 똑같게 한것, 산술 명령어는 항상 레지스터 피연산자 세 개를 갖도록 한 것, 어떤 명령어 형식에서나 레지스터 필드의 위치가 일정하게 만든 것 등이다.
- '작은 것이 더 빠르다.' MIPS 의 레지스터 개수를 32 개로 제한한 이유는 속도를 빠르게 하기 위해서이다.
- '자주 생기는 일을 빠르게 하라.' MIPS 에서 자주 발생하는 일을 빠르게 한 예로는 조건부 분기에 PC-상대주소를 사용한 것과 수치 주소로 상수 피연산자를 사용할 수 있게 만든 것 등을 들 수 있다.
- '좋은 설계에는 절충이 필요하다.' MIPS 의 경우, 명령어 내의 주소나 상수부는 클수록 좋으며 모든 명령어의 길이는 같은 것이 좋다는 두 요구사항을 적당히 절충하여 수용하고 있다.
이 기계어 수준 위에는 어셈블리 언어가 있다. 어셈블러는 이것을 기계가 이해할 수 있는 이진수로 번역하고, 때로는 하드웨어에 없는 명령을 추가하여 명령어 집합을 확장하기도 한다. 예를 들어 너무 큰 상수나 주소를 적당한 크기로 나누어 처리하며, 자주 쓰이는 명령어 시퀀스에 별도의 이름을 붙여 의사 명령어를 만들기도 한다.
각 명령어 종류는 다음과 같이 고급 언어의 구조나 문장과 연관지을 수 있다.
- 산술 명령어는 치환문에 나타나는 연산에 해당한다.
- 데이터 전송 명령어는 배열이나 구조체같은 자료구조를 다룰 때 자주 쓰인다.
- 조건부 분기는 if 문과 순환문에서 사용된다.
- 무조건 점프는 프로시져 호출과 복귀 및 case/switch 문에서 사용된다.
프로세서 : 데이터패스 및 제어유닛
프로세서의 데이터패스와 제어는 명령어 집합 구조와 기술의 기본적인 특성들을 이해하는 것에서 출발하여 설계할 수 있다.
어떤 구성요소가 데이터패스에 쓰일 수 있으며 단일 사이클 구현이 타당한지를 결정하는 등 기본 기술이 설계에 대한 많은 결정에 영향을 미친다는 걸 알았다.
비슷하게 제어는 많은 부분이 명령어 집합 구조, 구성 및 데이터패스 설계에 의해 정의된다. 단일 사이클 구성에서는 이같은 세가지 점이 제어신호들이 어떻게 설정되어야 하는지를 정의한다. 다중 사이클 설계에서는 명령어 실행을 몇 개의 사이클로 어떻게 나눌 것인가가 명령어 집합 구조와 데이터패스에 의존하는데 바로 이것이 제어에 대한 요구사항을 정의한다.
제어는 컴퓨터설계에서 가장 도전적인 문제 중 하나이다. 주요 이유는 제어를 설계하기 위해서는 프로세서의 모든 구성요소가 어떻게 동작하는지를 이해할 필요가 있기 때문이다. 이같은 어려운 작업을 돕기 위해 제어를 명시하는 두 가지 기법을 살펴보았다. 유한 상태 다이어그램과 마이크로프로그램이 그것이다.
이같은 제어에 대한 명세방법은 어떻게 제어를 구현할 것인가에 대한 상세한 내용과 구분하여 제어에 대한 명세를 추상화 할 수 있도록 해준다. 이같은추상화 작업의 사용은 컴퓨터설계의 복잡성을 헤쳐 나가는 주요방법이다.
일단 제어가 명세화되면 그것을 상세한 하드웨어로 사상할 수 있다. 제어 구현에 대한 상세한 점들은 제어의 구조와 이를 구현하는데 쓰이는 기본 기술에 의존한다. 제어 명세를 추상화하는 것은 값진 일인데 그 이유는 제어를 어떻게 구현할 것인가에 대한 결정은 기술에 의존적이며 시간이 지남에 따라 변할 가능성이 많기 때문이다.
파이프라이닝에 의한 성능 향상
파이프라이닝은 여러 개의 명령어가 중첩해 실행되는 구현기술이며, 프로세서를 빠르게 만드는 핵심기술이다.
예를 들어 MIPS 의 명령어 실행은 5 단계의 파이프라인을 사용한다.
- 명령어를 메모리로부터 가져온다.
- 명령어를 해독하는 동시에 레지스터를 읽는다(MIPS 명령어 형식은 읽기와 해독이 동시에 일어날 수 있도록 허용한다).
- 연산을 수행하거나 주소를 계산한다.
- 데이터 메모리에 있는 피연산자를 접근한다.
- 결과값을 레지스터에 쓴다.
파이프 라인 해저드
다음 명령어가 다음 클럭 사이클에 실행될 수 없는 상황이 있다. 이러한 사건들을 해저드(Hazard)라 부르는데 세 가지 종류가 있다.
구조적 해저드(Structural Hazard)
이것은 같은 클럭 사이클에 실행하기를 원하는 명령어의 조합을 하드웨어가 지원할 수 없다는 것을 의미한다. 예를 들어, 세탁소의 구조적 해저드는 독립된 세탁기와 건조기를 사용하지 않고 세탁기-건조기가 같이 붙어있는 기계를 사용했을 때 또는 방 친구가 다른 일을 하느라고 바빠서 빨래를 치워놓지 못했을 때 일어난다. 이렇게 되면 스케줄된 파이프라인 계획이 틀어진다.
데이터 해저드(Data Hazard)
한 단계가 다른 단계가 끝나기를 기다리기 때문에 파이프라인이 지연되어야 하는 경우 일어난다. 예를 들어, 건조된 옷을 접다가, 양말 한 짝이 없는 양말을 방에서 발견했다고 가정하자. 한가지 가능성 있는 방법은 방으로 달려가서 나머지 짝을 발견할 수 있는지 알아보기 위해 옷들을 뒤지는 것이다. 옷들을 뒤지고 있는 동안은 건조과정을 끝내고 접기과정을 기다리는 옷들과 세탁과정을 끝내고 건조과정을 기다리는 옷들은 기다려야만 하는 게 분명하다.
컴퓨터 파이프라인에서 한 명령어가 아직 파이프라인 상에 있는 앞선 명령어에 종속성을 가질 때 데이터 해저드가 일어난다.
제어 해저드
다른 명령어들이 실행중에 한 명령어의 결과값에 기반을둔 결정을 할 필요가 있을 때 일어난다. 어떤 세탁소 점원에게 축구팀의 유니폼을 세탁하는 즐거운 임무가 주어졌다고 생각하자. 세탁물이 얼마나 더러운지가 주어지면 우리가 선택하는 세제와 물 온도 설정이 유니폼을 깨끗하게 할 수 있을 정도로는 강해야 하지만 너무 강해 유니폼이 곧 닳아 떨어질 정도는 안 되도록 결정할 필요가 있다. 우리의 세탁소 파이프라인에서 세탁기 설정을 바꿀 필요가 있는지 아닌지를 알기 위해 건조된 유니폼을 조사하려면 두번째 단계까지 기다려야 한다.
분기 예측
당신이 유니폼을 세탁할 적당한 비율을 알고 있으면 그것이 잘 작용할 것이라고 예측하고 첫 번째 묶음이 건조될 때까지 기다리는 동안 두 번째 묶음을 세탁한다. 이같은 선택은 당신이 옳다면 파이프라인의 성능을 떨어뜨리지 않는다. 그러나 당신이 틀렸으면, 예측해서 세탁했던 묶음을 다시 세탁할 필요가 있다. 대부분의 컴퓨터는 분기 명령어를 다루기 위해서 예측(prediction) 을 사용한다. 한가지 간단한 방법은 분기가 항상 실패한다고 예측하는 것이다. 예측이 옳았으면 파이프라인은 최고 속도로 진행된다. 실제로 분기가 일어날 때만 파이프라인이 지연된다.
전방 전달(forwarding 또는 bypassing)
별도의 하드웨어를 추가하여 정상적으로 얻을 수 없는 항목을 내부자원으로부터 일찍 받아오는 것을 말한다.
지연(stall)
첫번째 묶음이 건조될 때까지 순차적으로 작업하면서 올바른 비율을 얻을 때까지 반복한다. 이같은 보수적인 방법이 효과가 있는 것은 확실하지만 느리다. 컴퓨터에서 이와 같은 결정에 관한 일이 바로 분기 명령어이다. 분기 명령어 다음 명령어를 바로 다음 클럭 사이클에 가져오기 시작해야만 한다. 그러나 파이프라인은 다음 명령어가 어느 것인지 알 가능성이 없다. 왜냐하면 방금 분기 명령어를 메모리에서 받았기 때문이다. 세탁소에서와 같이 한다면 한가지 가능한 해결책은 분기 명령어를 가져온 직후 지연시키는 것인데 파이프라인이 분기 명령어의 결과를 결정하고 다음에 어느 명령어 주소에서 가져올지 알 때까지 기다리는 것이다.
요약
파이프라이닝은 동시에 실행되는 명령어의 수를 증가시키며 명령어들이 시작하고 끝나는 속도를 증가시킨다. 파이프라이닝은 각 명령어의 실행을 끝내는데 걸리는 시간을 단축시키지는 않는데 이 시간을 실행시간(latency)이라고 부른다. 예를 들어, 5 단계 파이프라인은 하나의 명령어가 끝나는데 5 클럭 사이클이 걸린다. 앞에서 사용했던 용어로는 파이프라이닝은 개개의 명령어 실행시간(execution time 또는 latency) 보다는 처리율을 향상시킨다.
명령어 집합은 파이프라인 설계를 쉽게도 하고 어렵게도 한다. 파이프라인 설계자들은 이미 구조적 해저드, 제어 해저드, 데이터 해저드 등과 맞닥뜨려 이를 해결해왔다. 분기 예측, 전방전달, 지연은 올바른 값을 얻으면서도 컴퓨터를 빠르게 하는데 도움을 주는 기술이다.
크고 빠르게 : 메모리 계층구조
프로그래머들은 무제한의 크기를 갖는 빠른 메모리를 바래왔다. 여기서는 어떻게 무제한의 빠른 메모리를 갖고 있는 듯한 환상을 만드는지에 대한 매커니즘과 핵심원리를 알아본다.
시간적 지역성(Temporal locality)
만약 어떤 항목이 참조되면, 곧바로 다시 참조되기가 쉽다. 즉 여러분이 어떤 정보를 찾기 위해 책을 책상으로 가지고 왔다면, 곧바로 그 책을 다시 찾아보게 될 가능성이 매우 크다.
공간적 제약성(Spatial locality)
어떤 항목이 참조되면 그 근처에 있는 다른 항목들이 곧바로 참조될 가능성이 높다. 예를 들어, EDSAC 에 대한 내용을 찾기 위해 영국산 초기 컴퓨터에 대한 책들을 찾아 왔다면 서가의 그 책 주위에 초창기 기계식 컴퓨터에 관한 다른 책들이 있음을 알게 될 것이고 그 책 또한 가져와서 유용한 정보를 얻게 될 것이다. 같은 주제에 대한 책들은 공간적 지역성을 높이기 위해 서가에 함께 정리되어 있다.
프로그램은 최근에 접근했던 데이터들을 다시 사용하려는 경향, 즉 시간적 지역성과 최근에 접근했던 데이터에 인접해 있는 데이터들을 접근하려는 경향, 즉 공간적 지역성을 보인다.
캐쉬의 기본
하나의 워드로 구성된 블록을 사용하는 직접 사상 캐쉬, 이 캐쉬에서는 모든 워드가 분리된 태그를 가지고 있고, 하나의 워드는 단지 한 위치로만 사상되기 때문에 적중과 실패가 매우 단순하다. 캐쉬와 메모리를 일치시키기 위해서는 즉시 쓰기 방식이 사용되었다. 이 방식에서는 캐쉬에 쓰기가 발생될 때마다 메인 메모리에도 똑 같은 쓰기가 수행된다. 즉시 쓰기 방식의 대안으로는 캐쉬의 내용이 교체될 때만 블록이 메인 메모리로 쓰여지는 나중 쓰기 방식이 있다.
공간적 지역성을 이용하기 위해서는 캐쉬가 하나의 워드보다 더 큰 크기의 블록을 사용해야만 한다. 더 큰 블록의 사용은 실패율을 더욱 감소시키며, 캐쉬의 데이터 저장량에 비해 상대적으로 태그 저장량을 감소시킴으로써 캐쉬의 효율을 향상시킬 수 있다. 비록 더 큰 블록 크기가 실패율을 감소시키지만 실패 손실을 증가시키기도 한다. 실패 손실이 블록의 크기에 따라 선형적으로 증가한다면 큰 블록은 성능 감소를 가져온다.
이를 피하고, 캐쉬 블록을 더 효과적으로 전송하기 위해 메인 메모리의 대역폭은 증가 된다. 이를 위한 두 가지 방식에서는, 블록을 가져오기 위해 새로운 메모리 접근을 시작해야 하는 횟수를 줄임으로써 블록을 가져오는 시간을 줄일 수 있다. 또한 더 넓은 버스를 사용함으로써 메모리로부터 캐쉬로 블록을 보낼 때 필요한 시간을 줄일 수 있다.
캐쉬 성능의 향상과 측정
여기서는 세가지 주제를 다루었다. 캐쉬의 성능, 실패율을 줄이기 위한 연관정도의 사용, 실패손실을 줄이기 위한 다단계 캐쉬의 계층구조의 사용.
프로그램의 수행에 필요한 전체 사이클 수는 프로세서 사이클과 메모리-지연 사이클의 합이다. 메모리 시스템은 프로그램 수행시간에 큰 영향을 준다. 실제로 프로세서가 빨라짐에 따라(CPI 를 낮추거나 클럭속도를 증가시킴으로써) 메모리-지연 사이클의 상대적인 영향은 더 커지게 되었고, 좋은 메모리 시스템이 성능 향상에 있어 매우 중요하게 되었다. 메모리-지연 사이클 수는 실패율과 실패 손실에 의존한다.
실패율을 줄이기 위해 연관 배치 방식을 살펴보았다. 이 방식들은 캐쉬 내부에 보다 유연한 배치방식을 사용함으로써 실패율을 줄일 수 있었다. 완전연관 방식에서는 블록이 어디에나 위치할 수 있다. 그러나, 요청을 만족시키기 위해서는 캐쉬내의 모든 블록이 검색되어야만 한다. 이러한 검색은 캐쉬 블록마다 비교기를 갖게 하고 병렬로 검색함으로써 가능해진다. 비교기의 비용 때문에 큰 완전연관 캐쉬는 비실용적이다. 집합연관 방식이 실용적인 대안이다. 색인을 통해 유일하게 선택된 집합의 원소들만 검색하면 되기 때문이다. 집합연관 캐쉬는 적중률의 향상을 가져오지만, 집합의 원소들내에서 선택하고 비교하는 비용 때문에 접근시간이 약간 느리다. 직접 사상 캐쉬나 집합연관 캐쉬가 더 좋은 성능을 나타내는지는 기술과 자세한 구현 방식에 의존한다. 마지막으로, 주 캐쉬에서의 실패를 더 큰 2 차 캐쉬가 처리하도록 하여 실패 손실을 줄이는 기술로 다단계 캐쉬가 소개되었다. 설계자들이 제한된 실리콘의 크기와 고속 클럭속도 목표를 달성하기 위해서는 큰 주 캐쉬를 사용할 수 없다는 것을 파악하게 되면서, 2 차 캐쉬는 일반화되었다. 주 캐쉬보다 10 배 또는 그 이상 더 큰 2 차 캐쉬는 주 캐쉬에서 실패된 많은 접근을 수용할 수 있다. 이러한 경우에, 실패 손실은 2 차 캐쉬에의 접근시간(일반적으로 < 10 프로세서 사이클)이 된다. 메모리 접근시간은 일반적으로 100 프로세서 사이클 이상이다. 연관정도를 사용하여 설계자는 구현의 여러 측면에 의존하는 접근시간과 2 차 캐쉬의 크기 사이에서 최적의 선택을 위해 여러가지 구현 방법의 진단법을 분석하여야 한다.
가상 메모리
가상 메모리는 메인 메모리와 디스크 사이의 캐싱을 처리해주는 메모리 계층의 한 계층을 나타내는 이름이다. 가상 메모리는 하나의 프로그램이 메인 메모리의 한계를 넘어 주소공간을 확장시킬 수 있도록 해준다. 더 중요한 것은 최근의 컴퓨터 시스템에서는 가상 메모리가 다수의 동시에 활성화된 프로세스들(이들의 합은 존재하는 메인 메모리 크기보다도 훨씬 클 수도 있음)이 메인 메모리를 공유할 수 있도록 하여 준다는 것이다. 이러한 공유를 허용하기 위해 가상 주소는 메모리 보호를 위한 기능도 제공한다.
메인 메모리와 디스크 사이의 메모리 계층 구조를 관리하는 것은 페이지 부재의 높은 비용 때문에 쉽지 않다. 여러 기법들이 실패율을 줄이기 위해 사용된다.
- 페이지라고 불리는 블록들은 공간적 지역성을 이용하고 실패율을 줄이기 위해 크게 만들어진다.
- 페이지 테이블로 구현된 가상 주소와 실제 주소의 사상을 완전 연관 방식으로 구현하여 가상 페이지를 메인 메모리 내 어느 곳에나 적재시킬 수 있게 한다.
- 운영체제는 교체될 페이지를 선정하기 위해 LRU(가장 오래전에 참조된 순으로 페이지를 선택하는 방법) 와 참조 비트와 같은 기법을 사용한다.
디스크에 쓰기 또한 시간이 많이 걸린다. 그래서 가상 메모리는 나중 쓰기 기법을 사용하며, 변화되지 않은 페이지를 디스크에 저장하는 것을 피하기 위해 페이지의 변화여부(갱신비트)를 조사한다.
가상 메모리 시스템은 프로그램에 의해 사용되는 가상 주소를 메모리 접근에 사용되는 실제 주소 공간으로 주소 변환한다. 이 주소변환은 메인 메모리의 보호된 공유를 허락하고 메모리 할당을 단순화시키는 것과 같은 추가적인 기능을 제공한다. 프로세스들이 서로 보호되는 것을 보장하기 위해서는 단지 운영체제만이 주소 변환을 변경할 수 있어야 한다. 이는 사용자 프로그램이 페이지 테이블을 변경하지 못하게 함으로써 구현될 수 있다.
프로세스들 사이의 페이지의 제어된 공유는 운영체제와 사용자 프로그램이 페이지에 읽기와 쓰기 접근 권한을 갖고 있는지 여부를 말해주는 페이지 테이블의 접근 비트의 도움으로 가능하다.
만약 프로세서가 메모리 내에 있는 페이지 테이블을 모든 주소 변환을 위해 접근해야만 한다면 가상 메모리는 너무나 큰 부담을 갖게 된다. 대신 TLB 가 페이지 테이블 변환을 위한 캐쉬 기능을 한다. 그래서, 각 주소는 TLB 내의 변환을 가지고 가상 주소에서 실제 주소로 변환된다. 캐쉬, 가상 메모리, TLB 모두 공통되는 원칙과 정책에 의존한다.
결론
빠른 프로세서와 보조를 맞추도록 메모리 시스템을 만드는데 있어서 어려움은, 가장 빠른 컴퓨터나 가장 느린 컴퓨터에 쓰이는 메인 메모리, DRAM 의 재료가 본질적으로 같기 때문이다.
메모리 접근에 따른 긴 시간지연을 극복하는 것이 가능한 이유는 지역성의 원칙을 사용한다는 점이다. 이 원칙이 정확한 것은 메모리 계층 구조의 모든 계층에서 증명이 가능하다. 여러 프로세서에서 메모리 계층구조가 정량적인 측면에서 다르게 보이지만 동작 시에는 비슷한 전략을 사용하며 똑같은 지역성의 원칙을 적용하고 있다.
DRAM 접근이나 디스크 접근 시간보다 프로세서 속도가 더 빠르게 증가되었기 때문에, 메모리가 향상되고 DRAM 의 용량은 매 2 년 마다 2 배씩 증가하고 있다. 그러나, DRAM 의 접근 시간은 훨씬 더 느린 속도로 증가하고 있다(연간 약 10% 미만). 시간 지연이 느리게 향상되고 있지만 최근 DRAM 기술의 향상은 메모리 대역폭을 크게 증가 시켜주고 있다. 이와 같은 잠재적인 높은 대역폭은 설계자들이 실패 손실을 약간만 증가시키면서도 블록의 크기를 더 크게 증가시킬 수 있게끔 하여준다.
저장장치, 네트워크 그리고 다른 주변 장치
버스 및 입출력 장치, 프로세서, 메모리 간의 다른 연결방식
컴퓨터 시스템 내에서는 여러 종류의 서브 시스템들이 서로 연결되어야 한다. 버스는 한 다발의 전송선을 이용하여 여러 서브 시스템을 연결하는 공유 통신 링크이다. 버스 구성의 가장 큰 장점 두 가지는 융통성과 비용이다. 한 가지 연결 방식만 정의하면, 어떠한 주변장치도 새로 추가할 수 있을 뿐 아니라 같은 종류의 버스를 사용하는 시스템 간에는 주변장치를 서로 바꾸어 붙이는 것도 가능하다. 또 버스에서는 한 다발의 전송선을 여러 방도로 사용할 수 있으므로 비용 효용성이 높다.
버스의 가장 큰 단점은 통신 병목이 발생하여 입출력 처리량을 제한할 수 있다는 점이다. 프로세서의 요구에 맞출 수 있는 빠른 버스 시스템을 설계하는 것은 많은 입출력 장치를 연결하는 것 못지 않게 중요한 과제이다.
버스의 기초
버스의 기본적인 통신 방식에는 동기식(synchronous) 과 비동기식(asynchronous) 두 가지가 있다. 동기식 버스는 제어선에 클럭을 가지고 있어서 이 클럭을 기준으로 통신하는 고정된 프로토콜을 사용한다. 예를 들어 프로세서-메모리 버스를 통해 메모리에서 데이터를 읽을 때, 첫번째 클럭에서 주소와 함께 제어선을 통하여 읽기 명령을 보내고 다섯번째 클럭에서 메모리가 데이터를 보내는 프로토콜을 사용할 수 있다. 이런 프로토콜은 작은 유한상태기로 쉽게 구현할 수 있다. 이 프로토콜은 미리 정해져 있으며 필요한 하드웨어가 많지 않으므로 속도가 빠르고 인터페이스 회로도 간단하다.
그러나 동기식 버스는 두 가지 단점이 있다. 첫째로 버스 상의 모든 장치가 같은 클럭속도로 동작해야 한다. 둘째로 동기식 버스의 속도가 빨라지면 클럭 스큐 문제가 생기므로 버스 길이를 길게 할 수 없다. 프로세서-메모리 버스는 연결거리가 짧고, 장치 개수가 적으며, 연결되는 장치들의 속도가 빠르므로 동기식인 경우가 많다.
비동기식 버스는 클럭에 맞추어 동작하지 않는다. 그러므로 더 많은 종류의 장치를 수용할 수 있으며, 클럭 스큐나 동기 문제를 걱정할 필요 없이 길이를 늘릴 수 있다. Firewire 와 USB 2.0 은 둘다 비동기식 버스이다. 송신자와 수신자간의 조정을 위해 비동기 버스는 핸드쉐이킹 프로토콜을 사용한다. 핸드쉐이킹 프로토콜은 송신측과 수신측이 서로 합의했을 때만 다음 단계로 넘어가는 일련의 단계들로 구성된다. 프로토콜은 별도의 제어선을 써서 구현한다.
다음의 간단한 예를 통해 비동기 버스의 동작 방식을 살펴보자. 메모리 시스템에서 데이터 워드를 하나 읽어줄 것을 요청하는 주변장치를 생각하자. 다음과 같은 세가지 제어 신호가 있다고 가정한다.
- ReadReq : 메모리에 데이터 읽기를 요구하는 신호이다. 이 신호는 동시에 데이터선에 주소가 실린다.
- DataRdy : 데이터선에 유효한 데이터가 준비되어 있음을 표시한다. 출력 트랜잭션의 경우 메모리가 데이터를 보내면서 이 신호를 내보낸다. 입력 트랜젝션의 경우는 입력장치가 데이터를 보내면서 이 신호를 낸다. 어느 경우든지 이 신호와 동시에 데이터선에 데이터가 실린다.
- Ack : 상대방의 ReadReq 나 DataRdy 신호에 대한 확인 응답으로 사용한다.
비동기식 프로토콜에서 제어신호 ReadReq 와 DataRdy 는 상대방(메모리나 주변장치)이 이 신호를 보고 데이터선의 데이터를 읽었다고 Ack 신호선을 이용하여 표시할 때까지 계속 활성화된다. 이 전체 과정을 핸드쉐이킹이라 한다.
버스 대역폭은 동기식이냐, 비동기식이냐 하는 점과 버스의 타이밍 특성에 따라 대부분 결정되지만, 단일 전송시의 대역폭에 영향을 주는 요인들은 그밖에도 여럿이 있다. 그들 중 가장 중요한 것들은 데이터 버스 폭과 블록 전송을 지원하는지 그렇지 않은지의 여부이다.
결론
입출력 시스템들이 여러가지 다른 특성에 의해 평가된다(신용도). 지원하는 입출력 장치의 다양성; 입출력 장치의 최대 수; 비용; 시간 지연(latency)과 처리량(throughput)으로 측정되어진 성능. 이러한 목표들은 입출력 장치와의 인터페이스를 위한 다양한 방식을 출현시켰다. 저사양이나 중간사양의 시스템에서는 버퍼를 사용하는 DMA 가 가장 유력한 전송방식이 되고 있다. 고사양 시스템에서는 시간지연과 대역폭 둘 다 중요하게 되며 비용은 부수적인 문제일 것이다. 제한된 버퍼링을 사용하여 입출력 장치로 다중경로(multiple paths)를 제공하는 것이 고사양 입출력 시스템의 주요 특징이다.
일반적으로, 입출력 장치 상의 데이터를 상시 접근할 수 있도록 하는 것(높은 가용성)은 시스템이 발전함에 따라 더욱 중요해지고 있다. 그 결과 여유분(redundancy)과 에러보정 방식이 시스템이 확장됨에 따라 더욱 널리 사용되고 있다.
이 문서는
앞에서 살펴본 컴퓨터 구조를 바탕으로 X86 아키텍처의 어셈블리어를 역시 초심프로젝트의 일환으로 공부하면서 새롭게 알게된 사실을 기술하고 있다.
주교제 | 어셈블리 언어 |
IA-32 프로세서 구조
명령어 실행 주기
하나의 기계어 명령어의 실행은 명령어 실행 주기(instruction execution cycle) 라고 부르는 각 동작의 연속으로 나누어질 수 있다.
프로그램은 실행되기 전에 메모리로 탑재되어야 한다. 프로그램 카운터는 실행될 다음 명령어의 주소를 가지고 있는 레지스터이다.
명령어 큐(instruction queue)는 하나 혹은 그 이상의 명령어가 실행되기 전에 복사되어 있는 마이크로 프로세서 내부 영역이다.
중앙처리장치가 하나의 기계어 명령어를 실행할 때 추출, 해석, 실행의 3 가지 주된 동작이 필요하다. 실행되는 명령어가 메모리 피연산자를 사용한다면 피연산자 추출과 출력 피연산자 저장의 두 단계가 추가된다. 바꾸어 말하면, 메모리를 액세스하는 명령어는 5 개의 동작이 필요하다.
- 추출 : 제어장치는 명령어를 추출하여 중앙처리장치로 가져오고 프로그램 카운터를 하나 증가 시킨다.
- 해석 : 제어장치는 실행될 명령어의 타입을 결정하고 0 개 이상의 피연산자를 산술 논리장치로 보내며 실행될 동작의 유형을 알려주는 신호를 산술 논리 장치로 보낸다.
- 피연산자 추출 : 만약 피연산자가 필요하면, 제어장치는 메모리로부터 피연산자를 추출하기 위하여 읽기 동작을 시작한다.
- 실행 : 산술논리장치는 명령어를 실행하여 피연산자를 출력하고 상태 플래그를 갱신한다.
- 출력 피연산자의 저장 : 만약 출력 피연산자가 메모리에 있다면, 제어장치는 데이터 저장을 위해 쓰기 동작을 시작한다.
동작 모드
IA-32 프로세서는 기본적으로 보호모드, 실제주소 모드, 그리고 시스템 관리모드의 세동작 모드를 갖는다.
- 보호모드 : 보호모드는 모든 명령어와 특성을 이용할 수 있는 프로세서의 자연스러운 상태이다. 프로그램은 분리된 메모리 영역(세그먼트라고 부름)에 존재하고 프로세서는 하나의 프로그램에 할당된 영역 이외의 영역을 참조하려는 모든 시도를 탐지한다.
- 가상-8086 모드 : 보호 모드에서 수행 시, 프로세서는 안전한 멀티태스킹 환경에서 MS-DOS 프로그램과 같은 실제 주소 모드 소프트웨어를 직접 실행할 수 있다. 바꿔 말하면, 비록 MS-DOS 프로그램에 이상이 발생했어도 그때 실행되는 다른 프로그램에는 영향을 미치지 않는다.(비록 이러한 것이 하나의 새로운 프로세서 모드가 아닐지라도 종종 가상-8086 모드라고 부른다.)
- 실제 주소 모드 : 실제 주소 모드는 인텔 8086 프로세서의 프로그래밍 환경을 구현한 모드로서 다른 두 개의 모드로 전환할 수 있는 등의 몇 가지 새로운 특성을 가지고 있다. 이 모드는 예를 들어, 컴퓨터 하드웨어를 제어하는 MS-DOS 프로그램을 실행할 필요가 있을 때 윈도 98 에서 사용될 수 있다. 오래된 컴퓨터 게임이 종종 이런 경우이다. 모든 인텔 프로세서는 실제 주소 모드로 부팅한다. 이후 운영체제는 다른 모드로 전환한다.
- 시스템 관리 모드 : 시스템 관리 모드(SSM : system management mode) 는 운영체제가 전원 관리를 하거나 시스템 보안을 하는 등의 기능을 할 수 있게 한다. 이러한 기능은 보통 프로세서를 특별하게 설정하려는 컴퓨터 제조사에 의해 구현된다.
어셈블리 언어의 기초
정수 수식은 정수 상수, 심벌 상수 및 산술 연산자를 포함하는 수학식이다. 우선순위는 수식이 두 개 이상의 연산자를 포함할 때 동작하는 순서를 나타낸다.
문자 상수는 홑따옴표나 겹따옴표로 둘러싸인 하나의 문자이다. 어셈블러는 문자를 그 문자에 매칭되는 2 진 아스키 코드로 변환한다. 문자열 상수는 홑따옴표나 겹따옴표로 둘러싸인 문자의 열이며 보통 널 바이트로 종료된다.
어셈블리 언어는 예약어를 가지고 있는데 특별한 의미를 갖고 있으며 올바른 문맥에서만 사용된다. 식별자는 변수, 심벌 상수, 프로시저, 혹은 코드 레이블을 식별할 수 있는 사용자가 선택한 이름이다.
디렉티브는 프로그램의 소스 코드가 어셈블될 때 어셈블러가 인식하고 동작하는 명령이다. 명령어는 프로세서가 실행 시 실행되는 문장이다. 명령어 니모닉은 명령어에 의하여 수행되는 연산을 구분하기 위한 짧은 어셈블러 키워드이다. 레이블은 명령어나 데이터의 위치를 표시하는 하나의 식별자이다.
어셈블리 언어 명령어는 0 에서 3 개의 피연산자를 가질 수 있다. 피연산자는 레지스터나, 메모리 피연산자, 상수식, 입출력 포트 등이 될 수 있다. 프로그램은 코드, 데이터, 스택이라 이름 붙여진 논리 세그먼트를 갖는다. 코드 세그먼트는 실행 명령어를 포함한다. 스택 세그먼트는 프로시저의 매개변수, 지역변수와 리턴 주소를 갖고 있다. 데이터 세그먼트는 변수를 갖고 있다.
소스 파일은 어셈블리 언어 문장을 갖고 있는 문자 파일이다. 일람 파일(list)은 프로그램 소스코드, 줄 번호, 오프셋 주소, 변역된 기계어, 그리고 심벌표 등이 있어서 프린트하기에 적당하다. 맵 파일은 프로그램의 세그먼트 정보를 포함한다. 소스 파일은 문자 편집기로 만들어진다. 어셈블러(MASM)는 소스 파일을 읽어서 오브젝트와 일람 파일(list)을 만든다.
링커는 오브젝트 파일을 읽어서 실행 파일을 만든다. 실행 파일은 운영체제에 의하여 실행될 수 있다.
MASM 은 고유의 데이터 타입을 인식한다. 데이터 타입은 변수와 해당 타입의 수식에 할당될 수 있는 값들의 모음이다.
- BYTE 와 SBYTE 는 8 비트 변수를 정의한다.
- WORD 와 SWORD 는 16 비트 변수를 정의한다.
- DWORD 와 SDWORD 는 32 비트 변수를 정의한다.
- QWORD 와 TBYTE 는 8 바이트와 10 바이트의 변수를 각각 정의한다.
- REAL4, REAL8, REAL10 은 각각 4 바이트, 8 바이트, 10 바이트의 실수 변수를 정의한다.
데이터 정의 문은 변수를 위해 메모리 내의 저장공간을 지정한다. 이름은 선택적으로 할당된다. 다중 초기 설정자가 데이터 정의에 사용된다면 레이블은 첫 번째 바이트의 오프셋을 가리킨다. 문자열 데이터 정의를 만들기 위해서, 따옴표로 문자들의 열을 감싼다. DUP 연산자는 상수 표현을 카운터로 사용하여 반복적으로 저장공간을 할당한다. 현재 위치 카운터 연산자($)는 배열에서 바이트 수를 계산하기 위한 수식에 사용될 수 있다.
인텔 프로세서는 리틀-엔디언 순서를 사용하여 메모리로 데이터를 저장하든지 추출한다. 이것은 변수의 최소 유효 바이트가 메모리 주소의 가장 낮은 곳에 저장됨을 의미한다.
심벌 상수(심벌 정의)는 하나의 식별자(심벌)를 정수나 문자 수식에 연결하여 생성한다. 심벌 상수를 만드는 3 개의 디렉티브가 있다.
- 등호 디렉티브는 심벌을 하나의 정수 수식에 연결한다.
- EQU 와 TEXTEQU 디렉티브는 심벌 이름을 정수 수식이나 임의의 문자에 연결한다.
32 비트 보호모드와 16 비트 실제모드 프로그램간의 전환은 두 모드 사이의 약간의 차이만을 고려하면 쉽게 된다. 여기서는 두 타입을 지원하기 위해 동일한 프로시저 이름을 갖는 두 개의 링크 라이브러리를 제공한다.
데이터 전송, 주소지정, 연산
데이터 이동 명령어로서 MOV 는 소스 피연산자를 도착점 피연산자로 이동시킨다. MOVZX 명령어는 작은 피연산자를 큰 피연산자로 제로 확장한다. MOVSX 명령어는 작은 피연산자를 큰 피연산자로 부호 확장한다.
XCHG 명령어는 두 개의 피연산자의 내용을 맞교환한다. 적어도 하나의 피연산자는 레지스터여야 한다. 다음의 피연산자 타입이 여기서 소개되었다.
- 직접 피연산자는 변수의 이름으로서 변수의 주소를 표현한다.
- 직접 오프셋 피연산자는 변수의 이름에 변위를 더하여 새로운 오프셋을 만든다. 이 오프셋은 메모리 내의 데이터를 엑세스 하는데 사용될 수 있다.
- 간접 피연산자는 데이터의 주소를 포함하는 레지스터이다. 레지스터를 대괄호로 감싸서([esi]처럼) 프로그램은 주소를 역참조하여 메모리 데이터를 추출한다.
- 인덱스화된 피연산자는 간접 피연산자에 상수를 결합한다. 상수와 레지스터 값은 더해지고 결과 주소가 역참조 된다. 예를 들어, [array + esi] 와 array[esi] 는 인덱스화된 피연산자이다.
다음의 산술식은 중요한다.
- INC 명령은 피연산자에 1 을 더한다.
- DEC 명령은 피연산자로부터 1 을 뺀다.
- ADD 명령은 소스 피연산자를 도착점 피연산자에 더한다.
- SUB 명령은 도착점 피연산자로부터 소스 피연산자를 뺀다.
- NEG 명령은 피연산자의 부호를 바꾼다.
간단한 산술식을 어셈블리 언어로 바꾸는 것은 쉽다. 이렇게 할 때 어떤 수식이 먼저 계산될지를 선택하기 위하여 표준 연산자 우선순위 규칙을 따라야만 한다.
중앙처리장치 상태 플래그는 산술식에 의하여 영향을 받는다.
- 부호 플래그는 산술식의 결과가 음수이면 지정된다.
- 캐리 플래그는 부호 없는 산술 연산이 도착점 피연산자에 들어가기 너무 클 때 지정된다.
- 제로 플래그는 산술식의 결과가 0 일 때 지정된다.
- 오버 플로우 플래그는 부호 있는 산술식의 결과가 피연산자에 너무 클 때 지정된다. 중앙처리장치는 비트 6 에서의 캐리와 비트 7 에서의 캐리를 XOR 하여 검출한다.
이제 다음의 연산자가 어떻게 사용되는지 알아야만 한다.
- OFFSET 연산자는 세그먼트의 시작에서부터 변수의 거리가 얼마인지를 리턴한다.
- PTR 연산자는 변수의 디폴트 크기를 재설정한다.
- TYPE 연산자는 하나의 변수나 배열의 한 요소의 크기를 바이트로 리턴한다.
- LENGTHOF 연산자는 배열의 요소 수를 리턴한다.
- SIZEOF 는 배열의 초기 설정자가 사용한 바이트 수를 리턴한다.
- TYPEDEF 연산자는 사용자 정의 타입을 생성한다.
JMP 와 LOOP 명령어는 루프를 생성할 때 유용하다. 32 비트 모드에서 LOOP 명령어는 ECX 를 카운터로 사용한다. 16 비트 모드에서는 CX 를 사용한다. 16 비트와 32 비트 모드에서 LOOPD(더블 루프) 명령은 ECX 를 카운터로 사용한다.
프로시저
실행시에 스택은 주소와 데이터를 임시로 저장하기 위한 영역으로 사용되는 특별한 배열이다. ESP 레지스터는 스택 상의 특정 위치에 대한 32비트 오프셋을 갖고 있다. 스택은 LIFO 구조라고 부르는데 그 이유는 스택에 마지막으로 넣어진 값이 빠져 나올 때 첫 번째로 나오기 때문이다. 푸시 동작은 하나의 값을 스택에 복사한다. 팝 동작은 스택에서 하나의 값을 제거하고 그것을 레지스터나 변수에 복사한다. 스택은 프로시저의 리턴 주소, 프로시저 매개변수, 지역변수, 그리고 프로시저 내에서 사용하는 레지스터 등을 갖고 있다.
PUSH 명령은 먼저 스택 포인터를 줄이고 그 다음 소스 피연산자를 스택에 복사한다. POP 명령은 먼저 ESP 가 가리키는 스택의 내용은 16 비트 혹은 32 비트 도착점 피연산자로 복사하고 ESP 를 증가시킨다.
PUSHAD 명령은 32 비트 범용 레지스터를 스택에 푸쉬하고 PUSHA 명령은 16 비트 범용 레지스터에 대해 동일한 작업을 한다. POPAD 명령은 스택을 32 비트 범용 레지스터로 팝하며 POPA 는 동일한 작업을 16 비트 범용 레지스터에 대해 수행한다.
PUSHFD 명령은 32 비트 EFLAGS 레지스터를 스택에 푸쉬하고 POPFD 는 스택을 EFLAGS 로 팝한다. PUSHF 와 POPF 는 똑같은 작업을 16 비트 FLAGS 레지스터에 대하여 한다.
프로시저는 이름 붙여진 코드의 블록으로서 PROC 와 ENDP 디렉티브로 선언된다. 프로시저는 항상 RET 명령으로 끝난다. CALL 명령은 프로시저의 주소를 명령어 포인터 주소에 넣어서 프로시저를 실행한다. 프로시저가 종료되면 RET 명령은 프로세서를 그 프로세서가 호출되었던 프로그램으로 복귀하게 한다. 중첩된 프로시저 호출은 호출된 프로시저에서 리턴 전 다른 프로시저를 호출할 때 발생한다.
디폴트로 코드 레이블(하나의 콜론이 뒤에 온다)은 프로시저에서 지역적이다. :: 가 뒤에 오는 코드 레이블은 전역적 레이블로서 그 소스 코드 파일의 어디에서든 액세스할 수 있다.
USES 연산자는 PROC 디렉티브와 합쳐져서 프로시저에 의하여 수정되는 모든 레지스터를 나열한다. 어셈블러는 프로시저의 시작에서 그 레지스터를 푸시하고 리턴하기 전에 팝하는 코드를 생성한다.
프로그램은 명확한 사양으로부터 주의 깊게 설계되어야만 한다. 표준 접근 방법은 프로그램을 프로시저(함수)로 분해하기 위한 기능에 따른 분해(하향식 설계) 방법을 사용하는 것이다. 먼저 프로시저 사이의 순서와 연결을 결정하고 후에 그 프로시저의 세부 내용을 채우라.
조건부 처리
AND, OR, XOR, NOT, TEST 명령은 비트 수준에서 동작하기 때문에 비트 중심 명령어라 부른다. 소스 피연산자의 각 비트는 도착점 피연산자의 같은 위치에 매칭된다.
- AND 명령은 두 개가 모두 1 일 때 1 을 생성한다.
- OR 명령은 적어도 하나의 입력이 1 일 때 1 을 생성한다.
- XOR 명령은 입력비트가 다를 때 1 을 생성한다.
- TEST 명령은 묵시적 AND 연산을 도착점 피연산자에 가하고 플래그를 적절히 설정한다. 도착점 피연산자는 변화가 없다.
- NOT 명령은 도착점 피연산자의 모든 비트를 바꾼다.
- CMP 명령은 도착점 피연산자와 소스 피연산자를 비교한다. 그것은 도착점 피연산자에서 소스 피연산자를 빼고 중앙처리장치의 상태 플래그를 알맞게 수정한다. CMP 는 보통 코드 레이블로 제어를 옮기는 조건부 점프 명령어가 따라온다.
4 개의 조건부 점프 명령어 타입을 알아보았다.
- JC(jump carry), JZ(jump zero), JO(jump overflow) 등과 같은 특별한 플래그 값에 근거한 점프
- JE(jump equal), JNE(jump not equal), JECXZ(jump if ECX=0) 등과 같은 등호에 근거한 점프
- JA(jump if above), JB(jump if below), JAE(jump if above or equal) 등과 같은 부호 없는 정수의 비교에 근거한 조건부 점프
- JL(jump if less), JG(jump if greater) 등과 같은 부호 있는 점프
LOOPZ(LOOPE) 명령은 제로 플래그가 지정되고 ECX 가 0 보다 크면 반복한다. LOOPNZ(LOOPNE) 명령은 제로 플래그가 해제되고 ECX 가 0 보다 크면 반복한다.(실제 주소 모드에서 LOOPZ 와 LOOPNZ 는 CX 레지스터를 사용한다.)
암호화는 데이터를 암호화하는 절차이며 복호화는 데이터를 해독하는 절차이다. XOR 명령은 간단한 암호화 및 복호화의 실행 시 사용할 수 있다.
순서도는 프로그램 논리를 시각적으로 표현하는 효과적인 툴이다. 순서도를 모델로 사용하면 쉽게 어셈블리 언어 코드를 작성할 수 있다. 각 순서도 심벌에 레이블을 붙이고 그 레이블을 어셈블리 언어 소스 코드에 사용하는 것이 좋다.
유한 상태 기계는 부호있는 정수와 같은 인식 가능한 문자를 포함하는 문자열이 유효한지를 검사하는 효과적인 툴이다. 상태를 레이블로 표현하면 쉽게 유한 상태 기계를 어셈블리 언어로 구현할 수 있다.
.IF, .ELSE, .ELSEIF, .ENDIF 디렉티브는 어셈블리 코드를 상당히 단순하게 한다. 그들은 특히 복합적인 부울식을 코딩할 때 유용하다.
.WHILE 과 .REPEAT 를 사용해서 조건부 루프를 만들 수 있다.
정수 연산
시프트 명령은 어셈블리 언어의 가장 큰 특징 중의 하나이다. 숫자를 시프트 한다는 것은 그에 속한 비트들을 좌 또는 우로 이동시키는 것을 의미한다.
SHL(좌시프트) 명령은 도착점 연산자의 각 비트를 왼쪽으로 시프트하고, 최하위 비트를 0 으로 채운다. SHL 을 가장 잘 쓰는 곳 중 하나는 2 의 지수승인 수와의 고속 곱셈 연산을 하는 경우이다. 임의의 피연산자를 n 비트 좌시프트 하는 것은 2ⁿ 을 곱하는 것이다.
SHR(우시프트) 명령은 각 비트를 오른쪽으로 시프트하고, 최상위 비트를 0 으로 채운다. 피연산자를 n 비트 오른쪽으로 시프트하는 것은 그 피연산자를 2ⁿ 으로 나누는 것이다.
SAL(산술 좌시프트)과 SAR(산술 우시프트) 명령은 부호 있는 수에 대한 시프트를 위해 특별히 설계된 시프트 명령이다.
ROL(좌회전) 명령은 각 비트를 왼쪽으로 한 비트 시프트하고 최상위 비트를 캐리 플래그와 최하위 비트에 모두 복사한다. ROR(우회전) 명령은 각 비트를 오른쪽으로 한 비트 시프트하고 최하위 비트를 캐리 플래그와 최상위 비트에 모두 복사한다.
RCL(캐리 포함 좌회전) 명령은 각 비트를 왼쪽으로 시프트하고, 캐리 플래그를 최하위 비트로 이동하고, 최상위 비트를 캐리 플래그로 이동한다. RCR(캐리 포함 우회전) 명령은 각 비트를 오른쪽으로 시프트하고, 최하위 비트를 캐리 플래그로 복사한다. 캐리 플래그는 결과의 최상위 비트로 복사된다.
SHLD(더블 좌시프트) 명령은 목표 피연산자를 주어진 비트 수만큼 왼쪽으로 시프트 한다. SHRD(더블 우시프트) 명령은 목표 피연산자를 주어진 비트 수만큼 오른쪽으로 시프트한다. 이 두 명령은 모두 IA-32 부류의 프로세서에서만 허용된다.
MUL 명령은 AL, AX, EAX 와 8 비트, 16 비트, 32 비트 연산자를 각각 곱하는 명령이다. IMUL 명령은 부호있는 정수 곱셈을 수행한다. MUL 명령과 같은 구문과 같은 피연산자를 사용한다.
DIV 명령은 부호 없는 정수에 대한 8 비트, 16 비트, 32 비트 나눗셈을 수행한다. IDIV 명령은 DIV 명령과 같은 피연산자에 대한 부호 있는 정수 나눗셈을 수행한다.
CBW(바이트를 워드로 변환) 명령은 AL 의 부호 비트를 AH 레지스터까지 확장한다. CDQ(더블워드를 쿼드워드로 변환) 명령은 EAX 의 부호 비트를 EDX 레지스터까지 확장한다.
CWD(워드를 더블워드로 변환) 명령은 AX 의 부호 비트를 DX 레지스터까지 확장한다.
확장 덧셈과 뺄셈은 매우 큰 정수를 더하거나 빼는 것을 말한다. ADC(캐리 포함 덧셈) 명령은 시작점 피연산자와 캐리 플래그를 더해서 도착점 피연산자에 저장한다. SBB(빌림 포함 뺄셈) 명령은 도착점 피연산자에서 시작점 피연산자와 캐리 플래그의 값을 뺀다.
다음 명령들은 ASCII 10 진 정수(10 진수 스트링)와 압축되지 않은 10 진 정수를 사용하는 연산을 가능하도록 설계되었다.
- AAA(덧셈 후 ASCII 조정)명령은 ADD 또는 ADC 명령의 결과인 2 진수를 조정한다.
- AAS(뺄셈 후 ASCII 조정)명령은 SUB 또는 SBB 명령의 결과인 2 진수를 조정한다.
- AAM(곱셈 후 ASCII 조정)명령은 MUL 명령의 결과인 2 진수를 조정한다.
- AAD(나눗셈 전 ASCII 조정)명령은 나눗셈 연산 전에 AX 에 저장된 압축되지 않은 10 진수 형태의 피제수를 조정한다.
다음의 추가된 두 명령은 압축된 10 진 정수와 함께 사용한다.
- DAA(덧셈 후 10 진 조정)명령은 ADD 또는 ADC 의 결과(AL 에 저장됨)인 2 진수를 압축된 10 진 포맷으로 반환한다.
- DAS(뺄셈 후 10 진 조정)명령은 AL 에 저장된 SUB 또는 SBB 의 결과인 2 진수를 압축된 10 진 포맷으로 변환한다.
고급 프로시저
LOCAL 디렉티브는프로시저 내부에서 하나 또는 그 이상의 지역 변수들을 선언한다. 그것은 PROC 디렉티브 바로 다음에 위치해야 한다. 지역 변수들은 전역 변수들과는 다른 이점을 가지고 있다.
- 지역 변수의 이름과 내용에 대한 엑세스는 지역 변수를 포함하고 있는 프로시저로 제한된다. 지역 변수는 프로그램을 디버깅할 때 도움을 준다. 왜냐하면 오직 제한된 수의 프로그램 문장들만이 지역 변수들을 수정할 수 있기 때문이다.
- 지역 변수의 유효기간은 지역 변수를 포함하고 있는 프로시저의 실행 범위로 제한된다. 지역 변수는 메모리를 효율적으로 사용하게 한다. 왜냐하면 같은 저장공간이 여러 다른 변수들을 위해 사용될 수 있기 때문이다.
- 동일한 변수 이름은 이름 충돌없이 하나 이상의 프로시저에서 사용될 수 있다.
프로시저 매개변수에는 기본적으로 두 가지 타입이 있다. 레지스터 매개변수와 스택 매개변수. Irvine32 와 Irvine16 라이브러리들은 레지스터 매개변수를 사용한다. 레지스터 매개변수들은 프로그램 실행 속도를 위해 최적화되었다. 불행히도, 그들은 호출하는 프로그램 안에서 코드를 혼잡하게 하는 경향이 있다. 스택 매개변수들은 선택적이다. 프로시저 인수들은 호출하는 프로그램에 의해 스택에 푸시되어야만 한다.
INVOKE 디렉티브는 다중 인수들을 전달할 수 있게 하는 인텔의 CALL 명령에대한 보다 강력한 대안이다. ADDR 연산자는 INVOKE 디렉티브로 프로시저를 호출할 때, 포인터를 전달하기 위해 사용될 수 있다.
스택 프레임(활성화 레코드)은 프로시저의 리턴 주소, 전달된 매개변수 및 지역변수들을 위해 따로 설정된 스택 영역이다. 스택 프레임은 실행중인 프로그램이 프로시저 실행을 시작할 때 생성된다.
PROC 디렉티브는 매개변수의 일람을 갖는 프로시저 이름을 선언한다. PROTO 디렉티브는 존재하고 있는 프로시저에 대한 프로토타입을 생성한다. 프로토타입은 프로시저의 이름과 매개변수 일람을 선언한다.
변수 값의 복사본이 프로시저로 전달될 때, 값에 의한 전달이라 부른다. 변수의 주소가 프로시저로 전달될 때, 참조에 의한 전달이라 부른다. 호출된 프로시저는 주어진 주소를 통해 변수의 내용을 변경할 수 있는 기회가 주어진다. 고급언어에서는 참조를 통하여 배열을 함수의 인자로 넘기는데, 바로 어셈블리 언어에서도 동일한 방법이 사용된다. 다음은 몇몇 문제 해결 요령이다.
- 일반적으로 PUSH 와 POP 명령은 유용한 서비스를 수행한다. PUSH 와 POP 명령은 추후에 레지스터를 복구할 목적으로, 순차적인 명령 수행으로 변경될 레지스터를 보존하기 쉽게 해준다.
- 배열을 가지고 동작할 때, 주소는 배열 요소의 크기에 기초한다는 것을 명심하라.
- INVOKE 를 사용할 때, 어셈블러는 프로시저로 전달하는 포인터의 타입을 검사하지 않음을 명심해야 한다.
- 만약 프로시저가 참조 매개변수를 갖는다면, 즉시값(상수 값)을 인수로 전달할 수 없다.
MASM 은 다음과 같은 프로그램의 몇몇 중요 특성을 결정하기 위해 .MODEL 디렉티브를 사용한다. 메모리 모델 타입, 프로시저 명명 규약 및 매개변수 전달 규약. 지금까지 이 책에서 보여준 실제 주소 모드 프로그램은 모두 small 메모리 모델을 사용하였다. 왜냐하면 small 메모리 모델에서 모든 코드는 단일 코드 세그먼트 안에, 모든 데이터(스택 포함)는 단일 세그먼트 안에 유지시켜 주기 때문이다. 보호 모드 프로그램은 모든 오프셋이 32 비트인 flat 메모리 모델을 사용하며 코드와 데이터는 4GB 만큼 커질 수 있다. .MODEL 디렉티브에서 사용되는 언어 지정자는 C, PASCAL, STDCALL 이 될 수 있다.
프로시저 매개변수들은 EBP 레지스터를 가지고 간접 주소지정을 사용함으로써 액세스할 수 있다. [ebp+8] 과 같은 표현은 스택 매개변수 주소지정에 대한 더 높은 수준의 제어 방법을 제공한다. LEA 명령은 모든 종류의 간접 피연산자에 대한 오프셋을 리턴한다. LEA 는 스택 매개변수들과 함께 사용하기에 매우 적합하다.
ENTER 명령은 지역 변수들을 위한 스택 공간을 예약하고 스택 상에 EBP 를 저장함으로써, 호출된 프로시저에 대한 스택 프레임을 생성한다. LEAVE 명령은 이전 ENTER 명령의 동작을 역으로 수행시켜 프로시저를 위한 스택 프레임을 제거한다.
재귀적인 프로시저는 자기 자신을 직접적으로 또는 간접적으로 호출하는 프로시저이다. 재귀적인 방식은 반복적인 패턴을 갖는 자료 구조를 가지고 동작할 때 강력한 방식이 될 수 있다.
모든 소스 코드가 같은 파일 안에 있는 임의 크기의 응용 프로그램은 처리하기 어렵다. 프로그램을 여러 개의 소스코드 파일(모듈이라 부름)로 나누는것은 보다 더 편리하며 각 파일을 검토하고 편집하는 데 용이하다.
문자열과 배열
문자열 프리미티브 명령은 이상하게도 피연산자로 레지스터를 사용하지 않으면서, 고속 메모리 액세스를 위해 최적화되어 있다. 문자열 프리미티브 명령은 다음과 같다.
- MOVS : 문자열 데이터 이동
- CMPS : 문자열 비교
- SCAS : 문자열 검색
- STOS : 문자열 저장
- LODS : 문자열을 누산기에 탑재
각 명령은 바이트, 워드, 더블워드를 처리할 때, 각각 B, W, D 의 접미사가 붙는다.
REP 는 인덱스 레지스터를 자동으로 증가 또는 감소시키면서 문자열 프리미티브 명령을 반복한다. 예를 들어, REPNE 가 SCASB 와 함께 사용되면, EDI 가 가리키는 메모리의 값이 AL 레지스터의 내용과 일치할 때까지 메모리 바이트를 검색한다. 방향 플래그는 문자열 프리미티브 명령이 반복되는 동안 인덱스 레지스터를 증가시킬지 또는 감소시킬지를 결정한다.
실제적으로 문자열과 배열은 비슷하다. 전통적으로 문자열은 한 바이트인 ASCII 값의 배열로 구성되었지만, 이제 문자열은 16 비트 유니코드 문자의 배열이 될 수 있다. 문자열과 배열의 단 하나의 차이점은 일반적으로 문자열은 하나의 널 바이트(0) 로 종료된다는 점이다.
배열을 처리하는 것은 루프 알고리즘을 포함하고 있기 때문에 프로세서를 집중적으로 필요로 한다. 대부분의 프로그램은 전체 코드 중 적은 부분을 수행하는데 많은 시간(80 ~ 90%)을 사용한다. 따라서 루프 내의 명령의 숫자를 줄이거나, 명령의 복잡도를 줄여서 소프트웨어의 속도를 높일 수 있다.
어셈블리 언어는 사용자가 세세한 부분까지 제어할 수 있기 때문에 코드를 최적화하기에 좋은 툴이다. 예를 들어, 메모리 변수를 사용하는 대신 레지스터를 선택할 수도 있다. 또는 MOV 와 CMP 명령보다는 여기서 소개한 문자열 처리 명령 중 하나를 사용할 수도 있다.
베이스-인덱스 피연산자를 이용하여 2 차원 배열(테이블)을 쉽게 처리할 수 있다. 베이스 레지스터로 테이블의 행을 가리키도록 설정할 수 있고, 인덱스 레지스터로 선택된 행에서 열의 오프셋을 가리킬 수 있다. 범용 32 비트 레지스터는 베이스 레지스터와 인덱스 레지스터로 사용될 수 있다. 베이스-인덱스-변위 피연산자는 배열의 이름을 포함하는 것을 빼고는, 베이스-인덱스 피연산자와 비슷하다.
[ebx + esi] ; base-index array[ebx + esi] ; base-index-displacement
어셈블리 언어로 버블 정렬과 2 진 검색의 구현을 기술했다. 버블 정렬은 오름차순 또는 내림차순으로 배열의 원소를 정렬한다. 이 방법은 몇 백 개 이하의 원소를 갖는 배열에는 효과적이나, 큰 배열에는 비효율적이다. 2 진 검색은 정렬된 배열에서 하나의 값을 찾는 과정을 매우 빠르게 수행한다. 2 진 검색은 어셈블리 명령으로 작성하기 쉽다.
구조체와 매크로
구조체는 사용자가 정의한 타입인 패턴 또는 템플릿이다. 많은 구조체들이 이미 MS 윈도 API 라이브러리에 정의되어 있고, 응용 프로그램과 라이브러리 사이에 데이터를 주고 받기 위해서 사용된다. 구조체는 다양한 타입의 필드를 포함할 수 있다. 각 필드 선언에서는 필드에 디폴트 값을 할당하는 필드 초기 설정자를 사용할 수 있다.
구조체 그 자체는 메모리를 차지하지 않는다. 그러나 구조체 변수가 선언되면 메모리를 소모한다. SIZEOF 연산자는 변수에 의해 사용된 바이트 수를 반환한다.
도트 연산자(.)는 구조체 변수나 [esi] 와 같은 간접 피연산자를 사용하여 구조체의 필드를 참조한다. 간접 피연산자가 구조체 필드를 참조할 때, (COORD PTR[esi]).X 처럼 구조체 타입을 구분하기 위해 PTR 연산자를 사용해야만 한다.
구조체가 구조체인 필드를 포함할 때, 중첩된 구조체 정의라고 한다.
매크로는 일반적으로 프로그램의 시작 부분에 그리고 데이터와 코드 세그먼트 전에 정의한다. 그리고 나서 매크로 호출이 있으면, 전처리기는 각 매크로 코드의 복사본을 프로그램 상의 호출 위치에 삽입한다. 매크로는 프로시저 호출에 래퍼로서 효과적으로 사용될 수 있고, 매개변수의 전달과 레지스터의 저장을 간단하게 할 수 있다.
매크로 프로시저(또는 매크로)는 어셈블리 언어 문장들에 이름을 붙인 블록이다. 매크로 함수는 상수값을 반환하는것을 제외하고는 비슷하다.
IF, IFNB, 그리고 IFIDNI 와 같은 조건부 어셈블 디렉티브는 전달 인자의 영역, 빠진 것이 있는지 또는 잘못된 타입인지를 검사할 수 있기 때문에, 매크로에 효율성을 높여 준다. ECHO 디렉티브는 어셈블 동안 프로그래머에게 매크로에 전달된 인수에 오류가 있을 가능성을 경고하기 위해 오류 메시지를 화면에 출력한다.
치환 연산자(&)는 매개변수 이름의 모호한 참조를 해결한다. 확장 연산자(%)는 텍스트 매크로를 확장하고 상수수식을 텍스트로 변환한다. 리터럴-텍스트 연산자(<>)는 다양한 문자와 텍스트를 하나의 리터럴로 만든다. 리터럴-문자 연산자(!)는 전처리기가 미리 정의된 연산자를 일반 문자로 취급하도록 한다. 반복 블록 디렉티브는 반복적인 코드의 양을 줄여 준다.
- WHILE 디렉티브는 부울 대수식에 기반하여 문장 블록을 반복한다.
- REPEAT 디렉티브는 카운터의 값에 기반하여 문장 블록을 반복한다.
- FOR 디렉티브는 일련의 심벌에 대해 문장 블록을 반복한다.
- FORC 디렉티브는 문자열에 대해 문장 블록을 반복한다.
32 비트 윈도 프로그래밍
겉으로 보기에 32 비트 콘솔 모드 프로그램은 텍스트 모드로 동작하는 16 비트 MS-DOS 프로그램과 유사한 외형과 동작을 갖는다. 두 종류의 프로그램 모두 표준 입력에서 읽고 표준 출력으로 쓰기 동작을 하며, 명령어 상에서 리디렉션을 지원하고, 색깔이 들어 간 텍스트를 표시할 수 있다. 안으로 들어가 보면, Win32 콘솔과 MS-DOS 프로그램은 상당히 다르다. Win32 는 32 비트 보호 모드에 동작하지만, MS-DOS 는 실제 주소 모드에서 동작한다.
Win32 프로그램은 그래픽 윈도 응용에서 사용되는 것과 동일한 함수 라이브러리에 있는 함수들을 호출할 수 있다. MS-DOS 프로그램은 IBM-PC 의 도입 이래로 계속 존재해 왔던 BIOS 와 MS-DOS 인터럽트로 제한된다.
여기서 메모리 관리에 관한 부분은 논리 주소의 선형 주소로의 변환과 선형 주소의 물리적 주소로의 변환이라는 두 가지 주제에 초점을 맞추고 있다.
논리 주소는 세그먼트 서술자 테이블의 한 항목을 가리키며, 그것은 다시 선형 메모리에 있는 세그먼트를 가리킨다. 세그먼트 서술자는 세그먼트의 크기, 액세스 유형과 같은 세그먼트에 관한 정보를 포함한다. 서술자(Descriptor) 테이블에는 두 가지가 있는데, 단일 전역 서술자 테이블(GDT) 과 하나 이상의 지역 서술자 테이블(LDT) 이다.
페이징은 메모리에 모두 적재될 수 없는 프로그램을 컴퓨터에서 수행시킬 수 있게 하는 IA-32 프로세서의 중요한 기능이다. 프로세서는 처음에 프로그램의 일부만 적재하고, 나머지는 디스크에 보관하는 방식으로 이런 기능을 수행한다. 프로세서는 데이터의 물리적인 주소를 생성하기 위해서 페이지 디렉터리, 페이지 테이블, 페이지 프레임을 사용한다. 페이지 디렉터리는 페이지 테이블에 대한 포인터를 포함한다. 페이지 테이블은 페이지들에 대한 포인터를 포함한다.
다음은 페이징에서 주소변환의 순서이다.
- 선형 주소는 선형 주소 공간의 한 위치를 참조한다.
- 선형 주소에서 10 비트의 디렉터리 필드는 페이지 디렉터리 항목에 대한 인덱스가 된다. 페이지 디렉터리 항목은 페이지 테이블에 대한 베이스 주소를 가진다.
- 선형 주소에서 10 비트 테이블 필드는 페이지 디렉터리 항목으로 식별된 페이지 테이블에 대한 인덱스가 된다. 그 위치의 페이지 테이블 엔트리는 물리적인 메모리 상의 페이지에 대한 베이스 주소를 포함한다.
- 선형 주소에서 12 비트 오프셋 필드는 페이지의 베이스 주소에 더해져서, 피연산자에 대한 정확한 물리적인 주소를 생성하게 된다.
고급 언어 인터페이스
어셈블리 언어는 고급언어로 작성된 대형 응용(Application)에서 일부를 선택하여, 최적화를 위하여 사용하기에 매우 적절한 도구이다. 어셈블리 언어는 어떤 프로시저를 특정 하드웨어에 맞추는 도구로도 훌륭히 사용할 수 있다. 이 기법들은 다음의 두 가지 방법 중 하나를 이용한다.
- 고급 언어 코드에 내장되는 인라인 어셈블리 코드를 작성한다.
- 어셈블리 언어 프로시저를 고급 언어 코드와 링크한다.
두 가지 방법에는 모두 장점과 한계가 있다.
언어의 작명 규칙은 변수와 프로시저의 작명에 관한 원칙과 특성뿐 아니라, 세그먼트와 모듈의 이름을 붙이는 방법에도 관련된다. 프로그램의 메모리 모델은 (함수의) 호출과 (변수의) 참조가 near(동일한 세그먼트)인가 아니면 far(다른 세그먼트)인가를 결정한다.
다른 언어로 작성된 프로그램에서 어셈블리 언어 프로시저를 호출할 때, 두 개의 언어가 공유하는 모든 식별자는 호환되어야 한다. 또한 프로시저는 호출하는 프로그램과 호환되는 세그먼트 이름을 사용해야 한다. 프로시저의 작성자는 매개변수를 전달하는 방법을 결정하는 고급 언어의 호출 규약을 사용한다. 호출 규약은 호출되는 프로시저가 스택 포인터를 복구해야 하는지, 아니면 호출하는 프로그램이 복구해야 하는지에도 영향을 미친다.
비주얼 C++ 에서 __asm 디렉티브는 C++ 소스 프로그램에 인라인 어셈블리 코드를 작성하기 위하여 사용된다.
16 비트 MS-DOS 프로그래밍
MS-DOS 기본 메모리 구성, MS-DOS 함수 호출 방법, 그리고 운영체제 수준에서 기본 입출력 연산을 수행하는 방법을 살펴보았다.
표준 입력 장치와 표준 출력 장치를 합쳐 콘솔이라 부르며, 이것은 입력을 위한 키보드와 출력을 위한 비디오 디스플레이를 포함한다.
소프트웨어 인터럽트는 운영체제 프로시저를 호출한다. 인터럽트 처리기라는 이 프로시저들은 대부분 응용 프로그램에 입력-출력 기능을 제공한다.
INT(인터럽트 프로시저 호출)명령은 스택에 CPU 플래그를 저장하고 인터럽트 처리기를 호출한다. CPU 가 인터럽트 벡터 목록을 이용하여 INT 명령을 처리하고, 이 목록은 인터럽트 처리기의 32 비트 세그먼트-오프셋 주소를 포함한다.
프로그램이 수행될 때 명령어의 뒤에 친 문자가 128 바이트의 MS-DOS 명령어의 후미 영역에 자동적으로 저장된다. 이 영역은 프로그램 세그먼트 트레픽스(PSP) 라고 부르며 오프셋 80h 에 위치한다. 다음은 프로그램이 INT 명령을 호출할 때, CPU 의 동작을 단계적으로 보여준다.
- INT 니모닉 뒤의 숫자를 이용하여 인터럽트 벡터 목록에서 엔트리를 찾는다. 여기서는 INT 10h 명령이 실행된다.
- CPU 는 스택에 플래그를 저장하고 하드웨어 인터럽트를 금지한 다음, 인터럽트 벡터 목록에 저장된 주소(F000:F065)를 호출한다.
- F000:F065 번지의 인터럽트 처리기가 실행을 시작하고, IRET 명령에 도달하면 종료한다.
- IRET(인터럽트로부터 복귀) 명령은 호출 프로그램의 INT 10h 바로 다음에 있는 명령부터 프로그램이 다시 실행되도록 한다.
디스크의 기초
운영체제 수준에서 정확한 디스크의 물리적 위치나 기종에 따라 달라지는 디스크 정보를 알 필요가 없다. 디스크 제어기 펌웨어에 해당하는 바이오스가 디스크 하드웨어와 운영체제 사이의 중개자로서 동작한다.
디스크의 표면은 트랙이라는 보이지 않는 동심원으로 포맷된다. 데이터는 트랙에 자기적으로 저장된다. 평균 탐색 시간은 디스크 성능 표시 방법 중 하나이다. 또 다른 기준으로는 RPM 등이 있다.
실린더는 읽기/쓰기 헤드의 한 위치에서 액세스 가능한 모든 트랙들이다. 시간이 지나 파일이 점점 디스크에 흩어지면 파일이 단편화되고, 인접한 실린더에 저장되지 않는다.
섹터는 트랙의 512 바이트 짜리 조각이다. 제조사는 저수준 포맷이라는 방법을 이용하여 물리적인 섹터를 자기적으로(보이지 않게) 디스크에 표시한다.
물리적인 디스크의 기하학적 배치는 시스템 바이오스가 알 수 있도록 디스크의 구조를 기술한다. 하나의 물리적인 하드 드라이브는 파티션이나 볼륨이라는 하나 이상의 논리적인 장치로 나뉜다. 드라이브 네 개 이하의 파티션을 가질 수 있다. 하나가 확장 파티션이면 나머지 세 개는 주 파티션이 된다. 확장 파티션은 무제한의 논리적 파티션으로 다시 나누어 질 수 있다. 각각의 논리적 파티션은 별도의 드라이브 문자로 표시되고, 서로 다른 파일 시스템을 가질 수 있다. 주 파티션은 각각 부트 가능한 운영체제를 보관할 수 있다.
하드 디스크 상에 첫번째 파티션이 만들어질 때 생성되는 주 부트 레코드(MBR) 는 드라이브의 첫 번째 논리적 섹터에 위치한다. MBR 은 다음을 포함한다.
- 디스크 상의 모든 파티션의 크기와 위치를 나타내는 디스크 파티션 테이블.
- 파티션의 부트 섹터를 찾아, 운영체제를 탑재하는 프로그램에 제어권을 넘기는 작은 프로그램.
파일 시스템은 각 디스크 파일의 위치, 크기와 속성을 기억하고 있어야 한다. 파일 시스템은 논리적 섹터를 클러스터로 매핑하며, 모든 파일과 디렉터리의 기본 저장공간을 제공하고 파일과 디렉터리의 이름을 클러스터의 연속으로 매핑한다.
클러스터는 파일이 사용하는 공간의 최소 단위이며, 하나 이상의 인접한 디스크 섹터로 구성된다. 클러스터의 사슬은 파일이 사용하는 모든 클러스터를 기억하는 FAT 에 의해 참조된다. 다음의 파일 시스템들이 IA-32 시스템에서 사용된다.
- FAT12 파일 시스템은 IBM-PC 디스켓에서 처음으로 사용되었다.
- FAT16 파일 시스템은 MS-DOS 에서 포맷된 하드 드라이브에서만 사용 가능하다.
- FAT32 파일 시스템은 윈도 95 의 OEM2 에서 소개되었으며, 윈도 98 에서 다듬어졌다.
- NTFS 파일 시스템은 윈도 NT, 2000 그리고 XP 에서 지원된다.
각 디스크에는 루트 디렉터리가 있으며 디스크 상의 파일에 대한 주 목록 역할을 한다. 루트 디렉터리는 하위 디렉터리라고 부르는 다른 디렉터리의 이름을 포함할 수도 있다.
MS-DOS 와 윈도는 디스크 상에서 각 파일의 위치를 기억하기 위하여 FAT 라고 부르는 테이블을 사용한다. FAT 는 특정 파일의 연결 관계를 보여주는, 디스크 상의 모든 클러스터에 대한 지도이다. 각 엔트리는 클러스터 번호에 대응하며 각 클러스터는 하나 이상의 섹터를 포함한다.
실제 주소 모드에서 INT 21h 는 디렉터리를 생성하고 변경하거나, 파일 속성을 변경하고, 매칭 파일을 찾는 등의 유용한 함수들을 제공한다.
고급 언어에서는 이러한 함수들을 사용할 수 없을 가능성이 높다.
섹터 표출 프로그램은 디스크 볼륨에서 선택된 섹터를 읽고 화면에 표시한다. 디스크의 빈 공간 계산 프로그램은 선택된 디스크 볼륨의 크기와 빈 공간의 총량을 표시한다.
바이오스 수준에서의 프로그래밍
바이오스 수준에서 작업하면 MS-DOS 수준에 비하여 컴퓨터의 입력-출력 장치를 더 자유롭게 제어할 수 있다. 여기서는 INT 16h 를 이용하여 키보드를 다루는 방법, INT 10h 를 이용하여 디스플레이를 다루는 방법, INT 33h 를 이용하여 마우스를 이용하는 방법등을 배웠다.
기능 키와 커서 화살표 키 등과 같은 확장 키보드 키를 읽으려면 INT 16h 를 이용한다. 키보드 하드웨어는 키보드 입력을 프로그램에 제공하기 위하여 INT 9h, INT 16h 와 INT 21h 처리기 등의 도움을 받는다. 여기서는 키보드를 계속 조사하고 루프 바깥으로 빠져 나오는 프로그램을 소개하였다.
색은 원색을 가산 혼합하여 비디오 디스플레이에 표시된다. 컬러 픽셀은 비디오 속성 바이트에 매핑된다.
INT 10h 함수는 바이오스 수준에서 비디오 디스플레이를 제어하는 데 유용하다. 이 함수의 종류는 매우 다양하다. 이 장에서 컬러 창을 스크롤하고 가운데에 문자를 쓰는 예제 프로그램을 보여 주었다.
INT 10h 를 이용하여 컬러 그래픽을 그릴 수 있다. 간단한 공식을 이용하여 논리적인 좌표를 화면 좌표(픽셀 위치)로 변환할 수 있다.
예제 프로그램과 문서에서는 비디오 메모리에 직접 쓰는 방법을 이용하여 고속으로 컬러 그래픽을 그리는 방법을 보여주었다. 이 것은 INT 13h 를 사용하여 가능하다. INT 33h 함수들은 마우스를 조작하고 읽는다.
고급 MS-DOS 프로그래밍
자신의 세그먼트 이름을 사용하는 기존의 코드 라이브러리와 링크하려면 프로그래머가 명시적인 세그먼트 정의를 생성해야 하는 경우가 있다. SEGMENT 와 ENDS 디렉티브는 세그먼트의 시작과 끝을 정의한다. 정의된 세그먼트를 또 다른 세그먼트와 결합할 때, 정렬 방식은 몇 바이트를 건너 뛰어야 하는지를 링커에게 알려준다. 결합 유형은 동일한 이름의 세그먼트들을 결합하는 방법을 알려준다. 세그먼트 클래스 유형은 세그먼트들의 결합하는 또 다른 방법을 제공한다. 같은 이름을 부여하고 PUBLIC 결합 유형을 지정하면 여러 개의 세그먼트가 결합될 수 있다.
ASSUME 디렉티브를 이용하여 어셈블리 시에 레이블과 변수의 오프셋을 계산할 수 있다. 세그먼트 오버라이드 명령은 프로세서가 디폴트 세그먼트 이외의 세그먼트 레지스터를 이용하도록 한다.
MS-DOS 명령어 처리기는 명령어 프롬프트에서 친 명령어들을 해석한다. COM 과 EXE 확장자를 가진 프로그램을 비상주 프로그램이라 부른다. 이 프로그램들은 메모리에 탑재되고 실행된 다음에 사용한 메모리를 반납한다. MS-DOS 는 비상주 프로그램의 맨 앞에 프로그램 세그먼트 프레픽스라는 특수한 256 바이트짜리 블록을 만든다.
비상주 프로그램은 사용된 확장자에 따라 두 종류로 나누어진다. COM 과 EXE. COM 프로그램은 수정되지 않은, 기계어 프로그램의 2 진 이미지이다. EXE 프로그램은 EXE 헤더와 프로그램을 포함한 탑재 모듈을 합쳐서 디스크에 저장된다. MS-DOS 는 EXE 프로그램의 헤더 영역을 이용하여, 세그먼트와 다른 구성 요소들의 주소를 정확히 계산한다.
인터럽트 처리기(인터럽트 서비스 루틴)는 기본 시스템 작업뿐만 아니라 입력/출력을 단순하게 만든다. 디폴트 인터럽트 처리기를 다른 코드로 대체하여 더욱 완전하고 전용화한 서비스를 제공할 수도 있다. 인터럽트 벡터 목록은 RAM 의 첫 1,024 바이트(0:0 번지에서 0:03FF 번지까지)에 위치한다. 목록 내의 각 엔트리는 인터럽트 서비스 루틴을 가리키는 32 비트짜리 세그먼트-오프셋 주소이다.
하드웨어 인터럽트는 8259 프로그래밍 가능한 인터럽트 제어기(PIC) 에 의해 발생되며, 현재 실행중인 프로그램의 실행을 잠시 중단하고 인터럽트 서비스 루틴을 실행하도록 CPU 에게 신호를 보낸다. 하드웨어 인터럽트는 기본적인 데이터를 잃어버리기 전에, 중요한 이벤트가 발생했음을 CPU 에게 백그라운드로 알려 줄 수 있다. 인터럽트는 다른 장치에 의해 유발될 수도 있으며, 각 장치는 인터럽트 요구 수준(IRQ)에 의거하여 우선순위를 갖는다.
인터럽트 플래그는 CPU 가 외부(하드웨어) 인터럽트에 반응하는 방법을 제어한다. 인터럽트 플래그가 지정되면 인터럽트가 허용된다. 플래그가 해제되면 인터럽트는 금지된다. STI(인터럽트 지정) 명령은 인터럽트를 허용한다. CLI(인터럽트 해제) 명령은 인터럽트를 금지한다.
램상주 프로그램(TSR)은 자신의 일부를 메모리에 남긴다. 램상주 프로그램의 가장 일반적인 사용 방법은 컴퓨터가 다시 부트하거나 램상주 프로그램이 특수 프로그램에 의해 제거될 때까지 메모리에 상주하는 인터럽트 처리기를 설치하는 것이다.
여기서 소개한 No_reset 프로그램은 통상적인 Ctrl-Alt-Del 키에 의해 시스템이 재부트되는 것을 방지하는 램상주 프로그램(TSR)이다.
다음은 DOS 창에서 명령어를 입력했을 때, 발생하는 일련의 과정을 순서대로 나타낸 것이다.
- MS-DOS 는 명령어가 DIR, REN, ERASE 등과 같은 내부 명령인지 검사한다. 내부 명령이면 메모리 상주 MS-DOS 루틴에 의해 즉시 명령어가 실행된다.
- MS-DOS 가 COM 확장자를 가진 매칭 파일을 찾는다. 현재의 디렉터리에 있으면 실행한다.
- MS-DOS 가 EXE 확장자를 가진 매칭 파일을 찾는다. 현재의 디렉터리에 있으면 실행한다.
- MS-DOS 가 BAT 확장자를 가진 매칭 파일을 찾는다. 현재의 디렉터리에 있으면 실행한다. BAT 확장자를 갖는 파일은 일괄처리 파일이라고 부르며, 콘솔에서 명령어를 친 것처럼 실행되는 MS-DOS 명령어들을 포함하는 문자 파일이다.
- MS-DOS 가 현재의 디렉터리에서 매칭되는 COM, EXE, 혹은 BAT 파일을 찾지 못하는 경우에는, 현재의 경로에서 첫 번째 디렉터리를 검색한다. 거기에서도 적당한 파일을 찾지 못하면, 경로에서 다음 디렉터리를 차례로 검사한다. 그리고 매칭 파일을 찾거나 모든 경로 검색이 완료될 때까지 이 과정을 반복한다.
이번에는 키보드 인터럽트가 발생하였을 때의 순서를 나타낸 것이다.
- 스택에 플래그 레지스터를 저장한다.
- 인터럽트 플래그를 해제하여(CLI) 다른 하드웨어 인터럽트를 막는다.
- 스택에 현재의 CS 와 IP 를 저장한다.
- INT 9 에 대한 인터럽트 벡터 목록 엔트리를 찾아 이 주소를 CS 와 IP 에 넣는다.
다음에 INT 9 에 대한 바이오스 루틴이 실행되고, 다음과 같은 순서로 동작한다.
- 하드웨어 인터럽트를 다시 허용하여 시스템 타이머가 영향을 받지 않도록 한다.
- 키보드 포트에서 문자를 입력받아 키보드 버퍼에 저장한다. 키보드 버퍼는 바이오스 데이터 영역에 있는 32 바이트의 원형 버퍼이다.
- IRET(인터럽트로부터 복귀) 명령을 실행하여, IP, CS 와 플래그 레지스터를 스택에서 꺼낸 다음 인터럽트가 발생했을 때 실행중이던 프로그램으로 복귀한다.