티스토리 뷰

[전광성의 어셈블리어 이해하기:4회] 프로그래밍에 필요한 명령어와 디렉티브 (2)
저자: 전광성 |  날짜: 2005년 02월 04일  

/
1 .0 여러가지 명령어(MOV, 다이렉트-오프셋 오퍼랜드, ADD, SUB)
0 플래그와 연산자(OFFSET, PTR, TYPE, 인다이렉트 오퍼랜드) 및 배열
3 .0 JMP, LOOP 명령과 예제
/
  • 산술 연산에 영향을 받는 플래그

    산술 연산을 하다 보면 최대값을 뛰어넘어 오버플로우가 발생하기도 한다. 이럴 때 어떻게 처리를 해주어야 할까? 플래그(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)만큼 증가시켜줘야 한다는 것이다.
  • 댓글
    안내
    궁금한 점을 댓글로 남겨주시면 답변해 드립니다.