IT_Programming/Assembly

[전광성의 어셈블리어 이해하기] 프로그래밍에 필요한 명령어와 디렉티브

JJun ™ 2007. 7. 2. 10:32
LONG
 
 
  • 산술 연산에 영향을 받는 플래그

    산술 연산을 하다 보면 최대값을 뛰어넘어 오버플로우가 발생하기도 한다. 이럴 때 어떻게 처리를 해주어야 할까? 플래그(Flags)를 이용하면 된다. 플래그는 산술연산을 수행한 후, 어떤 상태가 되었는지에 대해 알려준다. 단지 오버플로우 뿐만 아니라 계산 후 0이 되었는지, 부호가 붙었는지 등을 표시해 준다.

    조심해야 할 것은 산술 연산을 두 번 수행하면 가장 최근의 상태만이 플래그에 남아있다는 것이다. 또한 특정 명령을 수행 한 후 플래그가 어떻게 변해 있을지 정의되지 않은 명령도 있으니 조심하기 바란다. 각 플래그는 1bit씩을 차지하며, 이러한 플래그를 모아놓은 것이 EFLAGS 레지스터이다. 이는 레지스터 메모리를 설명할때 이미 설명했었다.
      - 제로 및 사인 플래그(Zero and Sign Flags)
    먼저 제로 플래그는 ZF라고 흔히 표기하며, 명령 수행의 결과값이 0이 되었을 때 1(set)이 된다. 그렇지 않은 경우에는 0(clear)이 된다. 사인플래그는 SF라고 흔히 표기하며, 명령 수행의 결과값이 음수가 되었을 때 1(set)이 된다. 그렇지 않은 경우에는 0(clear)이 된다.
      - 캐리 플래그(Carry Flag)
    CF라고 표기하며, 부호 없는 정수 끼리의 계산 후에 오버플로우가 발생했는지 알아볼 수 있는 플래그이다. 캐리가 발생했다는 것은 덧셈을 하였을 때 그 결과가 결과값을 저장할 곳보다 더 큰 수일 때를 일컫는다. 결과값을 저장할 곳보다 더 큰 부분은 잘리게 된다. 예를 들어 두 8bit 피연산자의 덧셈 (1111 1111 + 0000 0001)을 하였다면 결과는 1 0000 0000 이 될 것이다. 결과값을 저장할 변수가 8bit라면, 이 연산에서는 오버플로우가 발생한 것이고 따라서 캐리 비트가 1이 되며, 결과값을 저장할 변수에는 0000 0000이 들어가게 된다.
      - 오버플로우 플래그(Overflow Flag)
    OF라고 표기하며, 부호 있는 수 끼리의 계산 후에 오버플로우가 발생했는지 알아볼 수 있는 플래그이다. 부호있는 정수형의 계산 후 OF가 1(set)된다면 그 연산에서 오버플로우가 발생한 것이다. 부호없는 정수의 연산도 오버플로우 플래그에는 영향을 미친다. 이게 무슨 말인고 하니, CPU는 우리가 부호 있는 정수를 연산한 것인지 부호 없는 정수를 연산한 것인지 모른다는 뜻이다. 그리고 각각의 명령에 따라 명령 수행 후 어떤 플래그에 영향을 미치게 될 것인지가 결정된다. 따라서 어떤 플래그를 보고 오버플로우 발생을 판단할 것인가는 프로그래머의 몫이다.

  • OFFSET 연산자

    < 그림 1 : OFFSET >

    OFFSET연산자는 데이터 레이블의 오프셋을 알아낼 때 사용된다. 오프셋은 특정 기준 위치로부터 떨어진 거리를 뜻하며, 특히 여기서는 데이터 세그먼트로부터의 거리를 이야기 한다. 그림1을 참고하면 좀 더 이해가 빠를 것이다. 포인터를 사용하려면 이 OFFSET연산자를 이용해서 데이터 세그먼트로부터의 거리를 알아내어야 한다. 구체적인 사용 예는 후에 인다이렉트 오퍼랜드(Indirect Operands)를 설명할 때 하겠다.

  • PTR 연산자

    PTR연산자는 피연산자의 사이즈(8bit/16bit/32bit)대신 임의의 사이즈로 읽어올 수 있다. 직접적으로 대응되는 것은 아니지만, 일종의 형 변환이라고 생각해도 무방하다. 예를 들어 보이겠다.

      .data
      myDouble DWORD 12345678h
      .code
      mov ax, WORD PTR myDouble     ; ax 는 5678h

    이 코드는 정상적으로 수행된다. 만약 WORD PTR을 뺀다면 어셈블러는 오류를 낼 것이다. 왜냐하면 첫번째 피연산자는 16bit레지스터라서 두번째 피연산자역시 16bit 레지스터나 메모리가 와야 하는데, 그렇지 않기 때문이다. 위에서 myDouble이라는 데이터 레이블에서 WORD만 읽어 내었다. 데이터 레이블은 해당 데이터의 시작 주소를 갖고 있는 심볼이므로, 우리는 ax에 1234가 들어가리라고 생각할 지 모른다.

    하지만 예전에 배운 리틀 엔디안 오더를 잊지 않았길 바란다. 12345678h에서 최하위 바이트는 78이다. 하위바이트부터 하위 주소의 메모리에 적재되므로 78 56 34 12이렇게 적재된다.(78쪽이 하위 주소의 메모리이고 12쪽이 상위 주소의 메모리이다)

  • TYPE 연산자

    TYPE 연산자는 사이즈를 바이트 단위로 리턴해 준다. TYPE의 뒤에는 변수명이 와야한다. 다음과 같이 데이터가 정의되어 있다고 하자. TYPE 연산자를 사용하면, 표1과 같은 값을 갖게 될 것이다.

      var1 BYTE ?     ; 초기화 하지 않는다는 표시
      var2 WORD ?
      var3 DWORD ?
      var4 QWORD ?

    수식

    Type var1

    1

    Type var 2

    2

    Type var 3

    4

    Type var 4

    8

    < 표 1 : TYPE 연산자 >

  • 인다이렉트 오퍼랜드(Indirect Operands)

    아까 설명한 OFFSET연산자로 오프셋을 레지스터 변수에 저장시킨 후 그 변수를 포인터로 사용할 수 있다. 다음의 예를 보자.

      .data
      var1 BYTE 10h
      .code
      mov esi, OFFSET var1
      mov al, [esi]     ; AL = 10h
      mov [esi], 0     ; var1 = 0

    이렇게 var1의 오프셋을 esi에 저장시킨 후 [esi]를 이용하여 간접적으로 var1을 사용할 수 있다.

  • 배열

    이제 배열을 사용하는 방법을 설명해 주겠다. 앞에 나왔던 것이므로 이해하기 어렵지 않을 것이다.

      .data
      array WORD 1000h, 2000h, 3000h     ; 워드형 원소가 세개인 배열
      .code
      mov ax, array     ; ax = 1000h
      mov ax, [array + 2]     ; ax = 2000h
      mov ax, [array + 4]     ; ax = 3000h

    [array + 2] 이렇게 한 것은 배열이 WORD형이기 때문이다. C에서 int ary[3]; 라고 했을 때 *(ary + 1)이렇게만 사용해도 두번째 원소에 접근할 수 있었던 것과 다르다는 것을 느낄 것이다. 어셈블리어에서는 주소 연산의 더하기는 데이터 형에 상관없이 항상 바이트 단위로 된다는 것을 잊지 않길 바란다.
    아래와 같은 방법으로도 배열을 사용할 수 있다.

      .data
      array WORD 1000h, 2000h, 3000h     ; 워드형 원소가 세개인 배열
      .code
      mov esi, OFFSET array
      mov ax, [esi]     ; ax = 1000h
      add esi, 2
      mov ax, [esi]     ; ax = 2000h
      add esi, 2
      mov ax, [esi]     ; ax = 3000h

    이전에 배운 인다이렉트 오퍼랜드(Indirect Operands)를 사용하였다. 조심할 것은 esi를 증가시킬 때
  • 원소 하나의 크기 (여기서는 WORD이므로 2)만큼 증가시켜줘야 한다는 것이다.


     

    ============================================================================================

     

     

  • JMP 명령

    JMP는 jump의 약자로, 코드의 특정 위치로 점프를 할 수 있다. C언어에서의 goto문과 정확히 일대일 대응이 되는 명령이다. "jmp 목적지의 코드레이블"와 같이 사용한다. 다음의 예를 보아라.

    L1: jmp L1

    지금 보여주는 것은 무한루프가 된다. jmp 앞에 붙은 L1:은 코드상에 붙인 레이블이다. jmp L1 이렇게 하게 되면 L1으로 가서 코드를 수행하게 된다. 그런데 L1으로 갔는데 또 jmp L1이 있다. 그렇다면 그 다음에도, 다음 다음에도 계속 jmp L1이라는 명령만 수행할 것이다. 따라서 무한루프를 돌게 된다.

    지금까지 배운 명령만 사용한다면 jmp명령어를 사용한다면 무조건 무한루프를 돌게 된다. 그것은 아직 우리가 조건에 따라 점프하는 명령을 배우지 않았기 때문이다. 후에 배울 것이니 일단은 jmp가 어떤 일을 하는 지만 알아 두기 바란다.

  • LOOP 명령

    LOOP명령은 말 그대로 루프를 돌리는데, 특정한 조건이 만족될 때 까지만 루프를 돌린다. LOOP역시 jmp처럼 코드 레이블을 인자로 받는다. LOOP명령이 수행되는 과정은 다음과 같다.

    1.일단 ecx를 1 감소시킨다.
    2.ecx가 0인가? 그렇다면 다음 명령을 계속 수행한다.
                        아니라면, 인자로 받은 레이블로 점프한다.

    간단한 사용예는 다음과 같다.

          mov ax, 0
          mov ecx, 5
    L1:
          inc ax
          loop L1

    위와같이 하면 루프를 빠져 나온 후 ax는 정확히 5가 되어있을 것이다. ecx에 5를 넣고 하나씩 감소시켜가며 0이 되었는지 확인하였기 때문이다. ecx는 여기서 한마디로 말해, 루프카운터의 역할을 하는 것이다. 잘 모르겠다면 LOOP명령 수행 과정을 다시 한번 훑어봐 주기 바란다.

    참고로, 코드가 수행되고 있을 때는 다음번에 수행할 명령에 대한 포인터가 레지스터 변수 EIP(Extended Instruction Pointer)에 저장되어있다. L1이라는 곳으로 점프를 한다는 것은 다음 수행할 명령에 대한 포인터가 L1레이블이 붙은 명령을 가리키도록 하는 것이다. 그래서 EIP는 프로그래머가 직접 건드릴 수 없도록 되어있다. EIP를 잘못 건드렸다가는 프로그램의 흐름 자체가 엉망이 되어버리기 때문이다.

  • 예제

    Include Irvine32.inc


    .data

    source BYTE "I love internet.com", 0

    target BYTE SIZEOF source DUP(0)


    .code

    main PROC

      mov esi, 0

      mov ecx, SIZEOF source  ; 루프 카운터

    L1:

      mov al, [source + esi]   ; source에서 한 글자를 읽어들인다.

      mov [target + esi], al     ; target에 저장한다.

      inc esi      ; 다음 글자를 가리키게 한다.

      loop L1


      ;target스트링을 출력하는 함수를 호출한다.

      mov edx, OFFSET target

      call WriteString


      exit

    main ENDP

    END main



  • 출력결과


  • 설명

    일단 코드의 맨 윗줄의 Include Irvine32.inc는 예전에도 설명했듯이 필자가 주로 참고하는 교재에서 제공하는 기본적인 프로시져들을 사용할 수 있도록 원형을 포함하고자 적어준 것이다. C에서 #include 를 하는 것과 같다고 생각하면 편할 것이다.

    그 다음에 .data는 디렉티브로서 이 부분에 변수를 적어주는 것이라고 설명했었다. 위에서는 source라는 배열에 "I love internet.com"을 집어넣고, 맨 마지막에 널문자를 넣어주었다. 그리고 target이라는 배열에 source와 같은 길이로 0을 채워주었다. 여기서 SIZEOF라는 것이 쓰였는데 이것은 연산자로서, source데이터 레이블로부터 시작되는 연속된 자료의 총 사이즈를 바이트로 리턴해준다. "N DUP(0)"은 괄호안의 수를 'N'개만큼 연속으로 선언한다는 뜻이다.

    다음에는 .code디렉티브로, 이 부분에 실제 수행할 코드를 적어준다. 그 아래는 main프로시져를 정의하는 부분이다. 여기서 일단 esi를 초기화 해 주는데, esi는 배열의 인덱스를 적어주게 될 것이다. C에 비유하자면, int ary[5]; 라고 선언한 후 esi = 0;한 다음 esi변수를 증가시켜가며 ary[esi]에 접근하는 것과 같은 역할을 한다. ecx는 루프를 돌리기 위한 카운터로, source배열의 전체 사이즈 만큼 루프를 돌릴 것이다. 따라서 mov ecx, SIZEOF source라고 값을 복사해 주었다.

    L1:은 코드 레이블로써 루프를 사용하기 위해 적어준 것이다. 루프의 구조는 간단하다. source + esi에서 한 바이트를 읽어 al에 저장하고, 이를 target + esi에 적어준다. 그리고 나서 esi를 하나 증가시켜 주는데, INC라는 명령을 사용하였다. INC는 increase의 약자로, 변수의 값을 하나 증가시켜준다.

    C에서 esi++;이라고 쓰는 것과 같은 의미이다. 그 다음 loop L1을 만나는데, 여기서 ecx를 하나 감소시킨 후 0인지 확인하고, 0이 아니라면 L1:레이블이 달린 곳으로 점프한다. ecx가 0이 되었다면 바로 다음 명령을 계속해서 수행하게 될 것이다. 나머지 코드는, 화면에 target배열에 들어있는 문자열을 찍어주기위한 프로시져호출이다. 뒤에서 프로시져를 배울 때 자세히 설명해 줄 테니, 지금은 어떤 역할을 하는 지만 알아두어라.

  • 마치는 글

    점점 설명이 어려워 지는 느낌이라 죄송한 마음 뿐이다. 쉽게 쓰려고 좀더 노력하겠다. 다음 회에서는 어셈블리어에서 프로시져를 사용하는 방법을 배울 것이다. 프로시져를 배우고 나면 고급언어에서 함수호출이 어떻게 런타임 스택을 이용하는지에 대해 이해할 수 있을 것이다. 또 약 3~4회 정도 후에 나올 고급 프로시져를 배운다면 좀더 완벽히 이해할 수 있다. 지난회를 읽지 않았다면 본회를 읽는데 어려웠을 것이고, 이번회를 읽지 않았다면 다음회를 읽는 데 어려움이 있을 것이다. 그럼 다음회에서 다시 뵙길 바란다.

  • ARTICLE
  • 시작하기에 앞서...

    시작한지 얼마 되지 않은 것 같은데 벌써 4회이다. 이번 회에서 배우게 될 내용은 다음과 같다.

      - 자료전송명령(C에서의 대입연산자(=)의 역할을 한다)의 사용
      - 덧셈과 뺄셈과 이에 관련된 부수적인 내용의 이해
      - 포인터와 배열의 사용
      - jmp명령(C에서의 goto)과 루프사용

    이제는 본격적으로 프로그래밍에 필요한 명령어와 디렉티브들을 배우게 될 것이다. 어렵게 생각하지 않았으면 하는 바램이다. 고급언어에서 나오던 개념을 조금더 깊게 생각해 보아야 할 것이다. 계속 언급하지만, 고급언와의 연계성을 생각해 가며 읽어주길 바란다.

    또 한가지 언급하고 싶은 것이 있는데, 본 강좌는 개념 파악에 주력할 것이며, 세세한 명령의 사용법이나 구체적인 부분에 대한 설명은 지양할 예정이다. 이는 본 강의를 부담없이 봐달라는 뜻과도 일맥상통한다. 따라서 코드 자체의 구체적인 부분까지 고민하지 않았으면 하는 바램이다.

  • 짚어두고 가야 할 개념

    우리는 지난 회에서 변수 이름은 단지 레이블이라는 것을 배웠다. 다시 설명하자면, 변수의 이름은 프로그래머가 알아보기 쉽도록 영문자로 되어있을 뿐이지 사실은 그 변수가 위치하는 메모리 공간의 주소와 대응된다. 그래서 '데이터 레이블'이라고 부르는 것이다. 다음과 같이 변수선언을 했다고 하자.

    var1 BYTE 10h

    우리가 다음 과 같은 명령을 내리려 한다고 치자.

    mov al, var1

    al레지스터에 var1변수의 값을 복사해서 넣으라는 명령이다. var1이 위치하는 메모리 주소가 10400h라고 하자. 이 코드가 어셈블된 다음에는 다음과 같은 명령으로 바뀌어 있을 것이다.

    mov al, [00010400]

    각괄호[]는 그 안에 있는 값을 주소로 인식하여, 그 주소가 가리키는 곳을 찾아가라는 의미이다. 허나, 실제로 어셈블리어 상에서 위와같은 구문으로 임의의 주소로 역참조 할 수는 없음에 유의하도록 하자. 만약 데이터 레이블이 없다면 우리는 데이터를 기록하고 읽어올 때 직접 그 메모리 주소로 접근해야 할 것이다. 얼마나 불편하겠는가? 또, 실수로 메모리주소를 잘못 적어 프로그램이 이상하게 돌아갈 경우에 디버깅 할 것을 생각하면 소름이 돋는다.

    여기서 한가지 짚고 넘어갈 것이 있다. 지금 필자는 계속해서 메모리 주소라는 단어를 사용해 왔다. 그런데 좀 더 구체적으로 이야기하자면 메모리 주소라고 하기 힘들다. 이것은 메모리 내의 데이터 세그먼트(메모리에 데이터를 쓰고 읽기 위해 잡아놓은 공간)의 시작 주소로부터 어떤 변수가 위치한 주소까지의 거리(Offset)라고 하는 것이 정확하다. 물론 지금 당장 이해하는 데에는 그 거리(Offset)를 그저 주소라고 생각해도 큰 문제 없지만, 정확히 알아두는 것이 좋을 것 같아서 지금 언급하는 것이다.

  • MOV 명령

    가장 기본이 되는 MOV명령이다. 어떤 어셈블리어 언어코드를 보더라도 가장 근간이 되는 명령이다. MOV명령은 피연산자를 두 개 갖는다. 첫번째가 데이터를 복사할 목적지이고 두번째가 데이터를 읽어올 곳이다. 예를 들어보이겠다.

    mov al, 0FFh

    위의 코드는 al레지스터에 16진수 FF값을 넣는다는 뜻이다. FF앞에 굳이 0을 써 준 이유는 그렇게 하지 않고 FFh라고 쓰면 FFh라는 데이터 레이블을 뜻하는 것으로 어셈블러가 착각하기 때문이다. MOV는 변수, 레지스터, 상수값 등을 피연산자로 취할 수 있는데, 조심해야 할 것은 메모리에 적재되는 변수끼리의 복사는 허용되지 않는다는 것이다. 따라서 레지스터 변수를 하나 이용하여 그것을 거쳐 복사해야 한다. 마지막으로, 두 피연산자의 사이즈가 서로 같아야 하므로 이에 유의하기 바란다.

  • 다이렉트-오프셋 오퍼랜드(Direct-Offset Operands)

    다음과 같이 변수를 선언했다고 하자.

    array BYTE 10h, 20h, 30h, 40h, 50h

    보다시피 array라는 레이블로 시작하는 연속된 메모리 공간에 10h, 20h, 30h, 40h, 50h가 순서대로 들어가 있다. 우리는 여기서 array라는 데이터 레이블을 사용하면 10h를 접근할 수 있다. 하지만, 그 뒤의 것은 어떻게 접근할까? 20h는 다음과 같이 접근할 수 있다.

    mov al, [array + 1]

    []는 역참조한다는 것을 명시적으로 표기해 주는 것으로서, 그 안에 있는 값이 가리키는 곳을 찾아가라는 의미이다. array는 데이터 레이블이라서 배열의 첫 번째 원소가 위치한 곳의 메모리 주소라고 하였다. 또, 위와같이 콤마로 데이터 선언을 한꺼번에 할 경우 연속적인 공간에 배치된다. 따라서 [array + 1]이라고 쓰면, array라는 데이터 레이블이 갖는 주소값에 1을 더한 곳을 역참조하므로, 20h가 된다. 같은 방식으로 [array + 2]라고 하면 30h가 된다.

    이와 같은 것을 다이렉트-오프셋 오퍼랜드(Direct-Offset Operands)라고 한다. array + 1을 하는 것이 array의 값을 1 증가시키는 것이 아님에 조심하여라. 변수의 값을 이용한 계산을 하려면 해당 계산의 인스트럭션을 이용해야 한다.

  • ADD 명령

    문자 그대로 덧셈을 수행하는 명령이다. 이 명령은 두개의 피연산자를 받아 덧셈을 하는데, 덧셈의 결과를 첫 번째 피연산자에 저장된다. 피연산자가 될 수 있는 조건은 MOV명령과 같다. 다음의 예를 보면 좀 더 쉬울 것이다.
      .data
      var1 DWORD 30000h
      var2 DWORD 10000h
      .code
      mov eax, var1
      add eax, var2 ; eax는 40000h이 된다.

    지난번에도 설명했지만, .data에는 전역 변수를 기록하게 되고, .code에는 실제 수행 코드가 들어가게 된다. 본래 main프로시져가 있어야 하지만 편의상 생략하였으니 헷갈리지 않도록 주의하라. 처음에 var1의 값을 eax로 복사하였다. 그 다음에 eax와 var2를 더한 값을 eax에 넣은 것이다.

  • SUB 명령

    SUB명령은 subtract의 약자로 뺄셈을 하는 명령이다. 이 역시 ADD와 마찬가지로, 피연산자로 올 수 있는 변수나 상수의 요건이 MOV와 같다. 첫번째 피연산자에서 두번째 피연산자를 뺀 후 그 결과를 첫번째 피연산자에 저장한다. 단순히 뺄셈 명령이므로 더 이상 설명할 것이 없다. 간단히 예를 들자면, 위의 ADD명령어 예제에서 add대신 sub을 사용하였다면 최종적으로 eax에 들어간 값은 20000h가 될 것이다.

    이것 하나는 알아두고 넘어갔으면 한다. 컴퓨터는 덧셈밖에 할 줄 모른다는 이야기를 들어 보았는가? 그렇다면 컴퓨터가 어떻게 뺄셈을 수행할까? 이것은 첫 번째 장에서 2의 보수 표현에 대해 설명할 때 언급하였다. 빼고싶은 값에 2의 보수를 취하여 음수로 만든 후 더해주면 된다. 예를 들어, 4 - 1을 수행하고자 할 때는 4 + (-1)을 계산하여 뺄셈을 한다.