어셈블리어

(Assembly에서 넘어옴)

Assembly

1 개요

프로그래밍 언어의 하나. 기계어에서 한 단계 위의 언어이며 기계어와 함께 단 둘뿐인 저급언어에 속한다.

기계어라는 게 컴퓨터 관점에서 바로 읽을 수 있다는 것 빼고는 인간의 관점에서는 사용이 불편한 언어이기 때문에 이를 보완하기 위해 나온 것이 어셈블리어다. 따라서 어셈블리어의 특징은 기계어 1라인당 어셈블리 명령어가 대부분 1라인씩 대응되어 있고 이를 비교적 간단하게 짤 수 있는 어셈블러를 통해 기계어로 변환되도록 한 것이다. 그리고 기계어는 CPU가 채택한 ISA에 따라 다 다르기 때문에 어셈블리어의 명령어 역시 통일된 규격이 없다. 또한 문법 아키텍처에 따라서도 다르고 어셈블러의 종류에 따라서도 문법/매크로 등이 제각각이다.

문법은 크게 인텔방식과 AT&T 방식으로 나눠진다. 인텔방식은 가독성이 뛰어나고, AT&T 방식은 가독성은 떨어지나, 인텔방식보다 좀더 많은 정보를 포함하고 있다고 선호하는 사람들도 있다. 사실, 간단한 수준이라면 뭘 쓰건 큰 차이는 없으나, 현대에 와서는 어셈블리어를 주력 프로그래밍 언어로 사용하는 경우는 MCU급의 소규모 프로그래밍 프로젝트인 경우가 많고 이런 시스템들은 주로 Windows환경에서 구동되는 인텔방식의 문법을 쓰는 전용 어셈블러IDE 환경에서 개발되는 경우가 많기 때문에 어셈블리어로 본격적인 코딩을 한다면 AT&T 방식보다는 인텔방식의 문법을 사용하게 되는 경우가 빈번해진다.

반면에 오픈소스 진영에서는 약간 이야기가 달라지는데 오픈소스 프로젝트의 경우는 대부분 스탠다드 컴파일러셋에 포함된 GNU 어셈블러(GAS)에서 AT&T 방식을 사용하고, 인텔방식은 근래들어 곁가지로 지원하는 정도이기때문에 GNU 컴파일러 툴과의 연동성을 위해서라면 부득이하게 AT&T 방식을 사용해야 한다. 하지만 오픈소스 쪽으로는 단독형 MCU를무섭으로 개발을 하는 경우보다는 리눅스 커널이나 부트로더 쪽에서 일부 사용되는 어셈블리어 코드를 약간 수정하는 경우가 대부분이 되기 때문에 아무래도 어셈블리어를 단독 프로젝트 수준으로 다루게 되는 경우는 드문 편이 된다.

2 장점

어셈블리의 장점은 많다.

001000 00001 00000 0000000000001010[1] 이라는 기계어를 addi $0, $1, 10[2]으로 단순히 바꾼 것뿐이지만 소리내서 읽을 수 있는 단어가 보이는 등(...) 가독성이 좋아졌다. 또한 프로그래머가 따로 주석[3]을 달 수 있게 되어 저런 외계어들이 뭘 의미하는지 나름대로 설명을 써 놓을수 있으니 프로그래밍을 하기 훨씬 수월해졌다.

또한 기계어에 대응되는 어셈블리 명령어 이외에도 메모리 위치나 정렬 등을 할 수 있는 지시어를 쓸 수 있고 매크로 기능을 이용하여 매크로함수 호출을 쉽고 편리하게 할 수 있다. 그로 인해 기계어로 직접 코딩할 때와 비교했을 때 보다 많은 소스코드가 포함된 프로젝트를 무리없이 개발할 수 있게 되었다.

그리고 특정 ISA에 대한 언어를 이해 하는데 들어가는 시간이 매우 짧다. 이 무슨 미친 소리냐고 반문할 수도 있겠지만 사실 어셈블리어는 CPU 매뉴얼의 부록 부분에 적혀있는 명령어 목록이 전부다. 그 외에는 사용하는 어셈블러의 매크로 문법 정도인데 문법의 종류는 CPU의 종류보다 훨씬 적으므로 한 번 익혀두면 대체로 재사용이 가능하다.

그리고 지금과는 다르게 1980~90년대에 어셈블리어로 코딩을 하면 저사양 PC에서도 처리속도를 올리는 게 가능했다. 단순히 처리속도 향상 수준이 아니라 8비트 시절에는 애플리케이션 개발에 어셈블리어는 필수로 여겨졌을 정도. 이는 80년대 당시에는 CPU들의 구조가 매우 단순했고, 그것들을 활용하는 컴파일러의 최적화 성능이 대체로 안좋은 데다가 아직 소스코드의 분량이 어셈블리어 프로그래밍을 사용하는 개발자들도 감당할 만한 수준이었다는 것에 기인한다. 덕분에 파스칼, C 같은 고급 언어로 최대한의 퍼포먼스를 요하는 액션이나 슈팅 게임을 만든다는 것은 상상하기 어려워서 이 시기에는 대부분의 컴퓨터, 아케이드, 콘솔 게임이 모두 어셈블리어로 제작되었다. 90년대에 들어 차츰 게임의 스케일이 커지고 컴퓨터의 구조가 복잡해지며 컴파일러의 최적화 성능도 개선되고 난 뒤에야 C 등의 고급언어로 이행했다.

3 단점

어셈블리어의 생산성이 높아지면서 보다 대규모의 소스코드를 다룰 수 있는 환경이 조성되었지만 그래봤자 어디까지나 기계어 대비 생산성이 높아졌을 뿐 상위 고급 언어에 비하면 생산성이 매우 떨어진다.

C언어 같은 고급언어에 비하면 단어가 심하게 축약되어 있어(addi는 add immediate[4]의 약자이다.)[5] 읽기에 좀 편한 수준이 된다. 그리고 CPU아키텍처의 관점에서 소스코드를 서술해야 하다 보니 정작 코드 작성자가 원하는 기능을 이해하기 쉽게 서술하기도 어렵다.
또한 어셈블리어는 언어를 이해하는데 들어가는 시간은 짧을지 몰라도 언어를 마스터하는데 들어가는 시간은 CPU종류별로 천차만별일 수 밖에 없는데 AVR과 PIC 등과 같은 단순한 CPU들은 CPU의 구조와 기계어 코드 구조가 매우 단순하여 CPU 명령어를 익히는 시간이 짧고 프로그래밍 시 CPU내부 아키텍처에 대한 깊은 지식이 필요치 않았으나, 90년대 이후의 x86을 위시한 고성능 PC및 워크스테이션용 CPU들은 파이프라인 기법이나 슈퍼스칼라 구조, 캐시 같은 온갖 속도향상 구조들이 도입되면서 CPU구조가 급격히 복잡해졌고 명령어들도 그에 따라 급격하게 복잡해졌기 때문에 충분한 기반지식 없이 어셈블리어를 효율적으로 쓰기가 힘들어졌기 때문.
결국 어셈블리어는 이미 90년대 들어오면서부터 그 사용 빈도는 고급언어들에게 밀려 거의 없다. 사실 과거엔 컴파일러가 그야말로 발적화에 가까웠기때문에[6] 어셈블리어를 이용하여 컴파일 된 코드를 고치거나 아예 어셈블리어로 프로그램을 짜는 등 사용하기는 했었으나 요즘은 PC 성능도 좋고 컴파일러도 좋아서 괜히 어셈블리어로 삽질할 필요는 거의 없다시피하다.[7]

4 사용

다만 시스템 드라이버라거나 임베디드 계열, 그중에서도 8비트급 CPU에서는 여전히 쌩쌩하게 현역으로 돌아가고 있다. 드라이버에서는 하드웨어를 제어하는 특성상 클럭과 타이밍을 맞춰야 하므로 타임 크리티컬한[8] 것들이 꽤 많다 보니 어셈블리로 하지 않으면 타이밍을 제대로 맞추기 어려운 것도 제법 되기 때문이고, 임베디드에서는 단가나 보드/칩 크기 문제로 8비트 CPU에 고작 램 몇 킬로바이트 정도만 쓰는 경우[9]도 많기 때문이다. 특히 램 크기가 작은 경우는 C컴파일러에서 생성하는 함수호출 프로시저 등에서 기본적으로 생성하지만 사용되지는 않는 코드들이 불필요하게 차지하는 램 용량도 성능저하를 일으킬 수 있기 때문에 어셈블리어의 사용이 유리하게 될 수 있다. 예로 들면 ATTiny칩 같은 경우에는 램이 몇 킬로바이트도 아니고 바이트 단위로 논다. 다만 이는 반대로 말하면 램 용량이 어느 정도여유가 있다면 생산성이 훨씬 높은 C등의 고급 언어를 쓰는 게 유리하다는 이야기이기도 하며 경험이 풍부한 프로그래머라면 둘 사이의 트레이드-오프를 적절하게 판단하여 프로젝트의 방향을 잡게 된다.

단순한 편인 연산을 무척이나 많이 반복하는 분야 - 예를 들면 Computer Vision - 에도 생각보다 많은 어셈블리어가 쓰인다. SIMD가 대표적이다. GPGPU를 사용하기에는 환경적으로 어려운 경우 - 가령, 모바일이나 GPU 성능이 좋지 못한 하드웨어에 해당 알고리즘들을 포팅해야 할 때 - 에는 특효약이 될 수 있다. OpenMP라든가, 일반적으로 빌드 시 컴파일러 옵션에서 SIMD 관련된 것들이 있지만, 이는 매우 제한적으로 적용되기 때문에 아직은 직접 사용자가 SIMD를 구현해야 한다. 다만, 어셈블리어를 직접 사용할 수도 있고, Intrinsics을 통해 좀 더 편한(?) C/C++ 스타일로 알고리즘을 구현할 수도 있다. 둘 간의 성능 차이도 생각보다 그렇게 크진 않다.

어셈블리어도 여러가지가 존재하는데, 요즘 나오는 어셈블리어는 꽤 고수준 명령들을 지원하는것들이 많고, 심지어 C 에 가까운 프로그래밍이 가능한것도 있다. 물론, 그만한 고수준 명령을 남발하면, 퍼포먼스도 C 만큼 떨어진다.등가교환 다만, 어셈블리어라는 타이틀을 위해서는 기계어와 1:1 대응이 되어야 하기때문에, 퍼포먼스를 원할경우 의식적으로 그런 고수준 명령들을 배제하고 쓰면 상관이 없다. 이런측면에서 보면 C 보다 고수준 어셈블리어가 나을수도 있지만, 어셈블리어는 결정적으로 명령어셋이 다른 플랫폼간 호환성 문제가 절대 해결이 안되는 치명적 단점이 있다.[10]

그 외에 게임이나 고성능이 필요한 작업에서 마지막 최적화 작업을 할 때 직접 어셈블리어 코드를 이용하기도 한다.[11]

중요한건, 어셈블리어를 사용한다고 꼭 퍼포먼스가 상승하는것은 아니다. 크고 복잡한 프로그램들일수록 이런 경향이 있는데, 간단한 작업만 하기에도 복잡한 어셈블리어로 크고 복잡한걸 만들어야 하는 경우, 버그없이 돌아가게 하는것만 해도 이미 엄청난 일이 돼버리는데, 여기에 일반적인 C 컴파일러가 해주는 최적화를 능가하는 수준의 최적화까지 하기란 웬만한 편집증이 아니고서야 사실 거의 불가능하다. 실제로, 역사가 좀 된 프로그램중에는 과거 어셈블리어로 짰다가 C 컴파일러들이 충분히 발달한 이후, C 언어로 다시 만들고보니 퍼포먼스가 오히려 크게 상승했다는 경우도 종종 있는데 이는 C컴파일러의 경우 최신 CPU 실행환경에 최적화된 바이너리 코드를 만드는 반면, 수제 어셈블리어 코드는 십수년전 기준의 CPU구조에 묶여 융통성 없는 바이너리 코드를 만들기 때문. 즉, 오늘날 임베디드 프로그래밍이 아닌 꽤 크고 복잡한 어플리케이션 프로그램에서 어셈블리어를 사용한다면 보통 고수준 언어들로 만들어 놓은 다음 퍼포먼스 툴로 보틀넥이 되는 부분을 찾아서 해당 코드의 어셈블리어 결과 obj파일을 분석하여 어셈블리어로 최적화 튜닝을 해주는정도. 실제로, 보통 프로그램에서 95% 의 코드는 런타임의 시간의 5% 만 차지한다는 격언이 있다. 즉, 런타임의 95% 를 차지하는 5% 정도의 핵심 코드를 찾아서 최적화를 해주는게 최적화의 핵심. 그렇기에 성능에 거의 혹은 전혀 영향을 안미치는 부분까지도 포함해서 퓨어 어셈으로 바닥부터 만드는건 일종의 바보짓이 된다.[12]

실제 컴퓨터 뿐만 아니라 가상 머신에도 어셈블리어가 사용된다. Java의 바이트코드와 닷넷 프레임워크의 CLR이 대표적인 가상 머신 어셈블리어. 다만, 가상 머신 어셈블리어는 다양한 자료형을 지원하거나 함수를 지원하는 등, 실제 어셈블리어와는 차이를 보이며, 대개 JIT 컴파일러를 거치거나 인터프리터를 통해 해석된다.

5 여담

어셈블리어를 기계어로 바꿔주는 프로그램을 어셈블러라고 한다. [13]

존 폰 노이만의 제자가 어셈블리어를 만들자 폰 노이만이 노발대발 했다는 이야기가 있다. 고작 그런 거로 컴퓨터님(…)의 성능을 낭비하려 한다나 뭐라나. 물론 이 당시 컴퓨터의 위상은 집채만한 크기면서 계산능력은 지금 계산기보다 딸리고 비싸기는 오라지게 비싸서[14] 국가 단위로 한두 개 있는 정도여서 인간 프로그래머는 컴퓨터에게 계산할 자료를 드리는 대컴퓨터콘택트용휴머노이드인터페이스에 불과했기 때문에 일어난 일. 물론 폰 노이만은 기계어로 자유자재로 코딩이 가능한 굇수였다는 점도 있다.그냥 그 머리로 직접 계산을 하시지 그러셨어요..라지만 저 인간은 진짜로 웬만한 건 다 암산한 괴수였다. 세월이 지나고 심하게 비효율적인 코드도 하드웨어빨로 씹고 실행하고, 컴퓨터 한 대가 프로그래머 한 달 월급보다 싸진 요즘은 프로그래머의 효율이 훨씬 중요하게 취급된다.

크리스 소이어가 이것을 사용해 트랜스포트 타이쿤롤러코스터 타이쿤 1, 2를 만들었다고 한다. 엄청난 괴수임이 분명하다(…). 그래서 그런지 몰라도 오브젝트가 난무하는 시나리오 후반부에도 음악파일을 가져올때를 제외하고 렉이 거의 존재하지 않을정도로 최적화가 잘 되어있다.[15] 또한 저사양에서도 무난하게 돌아가기 때문에 전세계적으로 많은 사랑을 받을수 있는 이유중의 하나가 되었다. [16]
그 그럼 무겐도?!
슈퍼패미콤의 게임들은 닥치고 어셈블리어로 개발할 수밖에 없었다. 사실 슈퍼패미콤 뿐만 아니라 메가드라이브, 패미콤, MSX 등 8, 16비트 세대에 나온 대부분의 게임은 거의가 다 어셈블리어로 개발되어있다. 몇몇은 자체 언어를 제작하는 등 예외를 두고 있기는 하지만. 당시의 하드웨어 성능으로는 아무래도 어셈블리어로 직접 쓴 코드와 컴파일러가 생성하는 코드의 퍼포먼스 차이가 꽤 컸기 때문이다. 물론 그 이전, 그러니까 아타리 쇼크 이전에는 게임 프로그래밍이 곧 어셈블리어 프로그래밍이고 시스템 프로그래밍이었던 시절인지라 어떤 게임기이든 닥치고 어셈블리어로 작성할 수 밖에 없었다. 그나마 요즘은 컴파일러 성능이 많이 좋아져서 굳이 어셈블리어로 삽질할 필요가 없어졌다.[17]

1980년대 컴퓨터 잡지(예를들어 일본 I/O 지)를 보면 프로그램의 어셈블리어 코드가 빽빽하게 인쇄된 페이지가 수백장씩 있었다. 독자는 그걸 한글자 한글자 직접 입력해서 프로그램을 복사...?하곤 했다. 물론 당시에도 카세트 테입 등의 저장매체가 있었으니 그런 것에 프로그램을 담아 배포할 수도 있었을 터이나, 이런 잡지의 주목적은 프로그래밍 언어의 학습이었으므로 코드를 직접 입력하며 그 내용을 공부하라는 의미가 있었고, 애초에 카세트 테이프를 읽을 수 있는 데이터 레코더마저도 갖추지 않은 컴퓨터도 많은데다, 지금은 거저 줘도 쓰지 않을 카세트 테이프도 당시 기준으로는 부록이라고 무료로 마음껏 배포할 수 있는 자원도 아니었다. [18] 인쇄물로 나눠주는 것이 가장 확실한 배포방법이기도 했던 셈. 여러모로 지금에 와서는 상상하기 힘든 일.

안철수가 만든 V3백신의 토대인 V1은 어셈블리어로 개발되어있다. 어셈블리어 굇수 2[19][20]

어셈블리어 코드를 짜는 시뮬레이션 게임이 스팀에 판매중이다... TIS-100. 너무 어렵게 생각한다면 Human Resource Machine을 해보는 것도 도움이 될것이다.

바람의 나라가 어셈블리어로 코딩되었다고 한다. 2016년 기준으로 20년이 지났지만 후대 개발자들은 갈아엎기도 어려우니 이 상태를 유지하며 개발하고 있다고 한다. 6.30버전 이후로 DirectX 를 지원하는 버전으로 모두 교체되었다.
  1. MIPS 아키텍처의 기계어. 16진수로 바꾸면 08 01 00 000A.
  2. 리눅스의 부트로더에도 사용되는 AT&T 어셈블리어 구문이다. MASM 계열은 이것과 약간 다르다. 해석하자면 "0번 레지스터(기억장치)에 들어있는 값에 10을 더해서 1번 레지스터에 넣어라"($1 = $0 + 10) 라는 뜻이다.
  3. 프로그래머가 어셈블러 또는 컴파일러에게 인식되지 않게 한 부분
  4. '상수'라는 뜻이다. 윗 단락 예시에서 10을 더했듯이 고정된 수를 더할 때 쓰인다. 기억장치에서 숫자를 불러올 필요 없이 바로(immediately) 더할 수 있기 때문에 immediate라는 이름이 붙었다.
  5. 물론, C 언어도 만들어질 당시에 에디터의 자동완성 기능같은건 상상할수도 없었기때문에 오늘날 함수명이나 변수 하나가 심하면 한줄씩 차지하는 언어들에 비하면 타수를 줄이기 위하여 strxfrm(string transformation) 이나 fma(fused multiply-add) 등의 라이브러리 함수명처럼 축약형을 애용하는 전통을 따르는편이다.
  6. 어느정도로 발적화였냐면, while(1){ ... } 이 for(;;){ ... } 보다 느린 컴파일러가 많았다. 오늘날 컴파일러는 둘다 무한루프인걸 알기때문에 속도가 같지만, 과거 컴파일러의 경우에는 while 문의 경우 루프를 돌때마다 항상 저 1 을 테스트했기때문에 속도가 크게 떨어졌다. 때문에, 연식이 좀 된 프로그래머들의 경우 지금도 습관적으로 while(1) 보다 for(;;) 를 애용하는 경우를 종종 볼 수 있다.
  7. 요즘은 괜히 어셈블리어 좀 안다고 깝치다가 컴파일러만도 못한 발적화 코드를 내놓는 경우도 비일비재하다. 오래된 CPU와 달리 명령어 실행 시간이 조건에 따라 달라지기 때문에 사람이 그걸 다 따져가며 작업하기가 그만큼 힘들기 때문이다. 물론 컴파일러로는 절대로 할 수 없는 유형의 최적화도 존재한다.
  8. 대표적으로 유도 미사일. 유도 미사일은 정해진 시간 안에 방향 계산을 완료하지 못하면 목표물을 격파시킬 수 없다.
  9. BSS 참조
  10. 물론, C 의 호환성도 따지고보면 미신이라고 반박하는 경우도 있는데 그래도 거의 완전히 갈아엎어야 하는것과 부분부분 바꿔주면 되는것의 차이는 크다.
  11. 이 경우는 보통 컴파일한 바이너라나 실행코드의 메모리 주소를 포인터로 찍고 기계어로 출력한후 이것을 역어셈블한 결과물을 베이스로 작업한다. 어셈블리어는 기계어와 1:1 대응이 되기때문에 바이너리 ↔ 어셈블리어 양방향 전환이 자유로운편이다.
  12. 물론, 해당 프로젝트에 인생을 바칠 각오가 되어있다면 상관없다.
  13. 컴파일러는 말그대로 편집, 또는 수집을 한다는 소리이다. 예를 들어, C언어의 컴파일러는 소스 코드를 목적 코드로 바꾸고, 오프젝트 파일로 바꾼후, 여러 컴파일에 필요한 파일을 모은후에 바이너리로 만들어 출력을 한다. 어셈블리를 기계어로 바꾸는 과정에는 그런거 없다. 그러기에 어셈블러와 컴파일러로 나누는 것이다.
  14. 그 당시가격으로 50만달러인데 지금 가치로는 6백만달러. 72억원정도이다. 애니악 한대가 그 가격.
  15. 물론 아무리 어셈블리어로 짜도 못짜는 사람은 더럽게 렉걸린다.물론 어셈블리어를 다룰줄 아는 사람이면 그만큼 괴수니까 그럴 가능성은 낮지만. 여하튼 크리스 소이어의 실력이 대단하다는걸 알수있다.
  16. 트랜스포트 타이쿤은 386DX에서도 돌아간다! 오오 크리스 소이어 오오
  17. MS의 매크로 어셈블러와 베이직 인터프리터는 IBM PC와 함께, 그리고 볼랜드의 터보 시리즈 컴파일러가 8비트 컴퓨터용으로 출시됐지만, 당시 컴퓨터책과 잡지에서는 C언어를 저급언어라 부르면서도 어셈블리어 강좌를 연재했고, 개인프로그래머는 변환테이블을 보고 핸드어셈블한 걸 기계어 모니터 프로그램으로 직접 입력하기도 했다. 디버그는 트레이스(TRON, TROFF)를 이용해 레지스터와 포인터, 플래그를 직접 보며 근성으로!
  18. 90년대 초반까지만 해도 조금이라도 돈을 더 아끼려고 오래된 어학 테이프 등을 지우고 음악을 녹음해서 담고 다니던 시절이 있었다.
  19. [1]에서 발췌
  20. 다만 이진 파일 속에서 바이러스를 찾는다는게 딱히 기계어나 어셈블리어와 상관이 있는건 아니다. 이런 경우는 바이러스가 파일 내부를 건드리는 부분만 잡아서 검출만 할 수 있으면 되기 때문에 파일 자체의 구조 분석이 더 자주 쓰인다. 그 시대 프로그램들이 다 그랬듯이 V1도 그 흐름을 따라서 어셈블리어로 개발 되었을 뿐이다.