IT_Programming/Assembly

[전광성의 어셈블리어 이해하기] 정수 산술연산(이진연산)

JJun ™ 2007. 7. 2. 10:38
LONG
  • 쉬프트와 로테이트 인스트럭션

    쉬프트(Shift)는 비트의 열을 왼쪽으로 또는 오른쪽으로 움직이는 것을 뜻한다. 로테이트(Rotate)는 쉬프트와 유사하지만, 쉬프트할 때와 같이 움직이면서 버려지는 비트가 빈 곳으로 다시 채워진다. 이렇게 말로 풀어서는 잘 이해가 안될 것이다. 여기서는 먼저 숲을 보도록 하고, 후에 나무를 볼 때, 그림과 곁들어 설명하겠다. 다음 표를 보면 뭔가 분류가 될 것이다.


    여기서 눈여겨 볼 것은 논리 쉬프트(Logical Shift)와 산술 쉬프트(Arithmetic Shift)이다. 위의 표에서 다른 말 없이 shift라고 쓴 것은 논리 쉬프트를 말하는 것이다. 논리 쉬프트에서는 쉬프트를 하여 새로 생긴 빈 공간에 0을 집어 넣는다. 반면, 산술 쉬프트에서는 새로 생긴 빈 공간에 이전에 존재하던 값을 다시 넣는다. 왜 이름이 산술 쉬프트인지, 또 어떤 역할을 하게 되는지는 후에 설명할 것이다.

  • SHL, SHR 인스트럭션

    논리 쉬프트를 하는 인스트럭션이다. 다음 그림 1을 보면 좀더 이해가 빠를 것이다.


    << 그림 1 : SHL>>

    보다시피 쉬프트를 하고 나서 빈 자리에는 0이 들어오는 것을 볼 수 있다. SHL은 피연산자를 두 개 받는데, 첫번째 것은 쉬프트할 대상이고, 두번째 것은 쉬프트할 횟수이다. SHL ax, 3 이라고 하면 ax레지스터에 있는 값을 왼쪽으로 3칸 쉬프트 하라는 뜻이다. 당연히 빈자리에는 0이 채워질 것이다. 그리고 그림 1에서 보는 바와 같이, 쉬프트하면서 밀려난 비트는 Carry Flag에 들어가게 된다. SHR도 마찬가지이므로 따로 설명하진 않겠다.

  • SAL, SAR인스트럭션

    산술 쉬프트를 하는 인스트럭션이다. 다음 그림 2를 보라.


    << 그림 2 : SAR>>

    SAR에 대한 그림이다. 오른쪽으로 쉬프트를 하는데, 빈 자리는 원래 있던 값을 그대로 복사해 넣는 것이다. 쉬프트하면서 버려지는 값은 Carry Flag에 들어간다. 사용 형식은 SHL, SHR과 같으므로 생략하겠다. 또한 SAL은 SHL과 완전히 같고 이름만 다르다. 왜 그런지는 다음 단락에서 설명해 주겠다.

  • 왜 산술 쉬프트인가?

    산술쉬프트의 이름은 왜 산술 쉬프트인가? 그것은 쉬프트가 산술 연산중 곱셈과 같은 역할을 할 수 있기 때문이다. SAL(==SHL)을 한 번 하는 것은 2를 곱하는 효과가 있다.
    mov dl, 5
    shl dl, 1

    이런 코드를 수행하게되면 dl에 있는 값은

    수행 전 : 0000 0101 (5)
    수행 후 : 0000 1010 (10)


    이렇게 된다. 보다시피 곱하기 2를 한 효과이다. 만약 10을 두 번 쉬프트 한다면 0000 1010 에서 0010 1000이 되므로 40이 된다. 부호있는 정수의 곱도 물론 할 수 있다. 즉, 4를 곱한 효과가 있다. 당연히 mul연산을 이용해 곱하는 것 보다 빠를 것임을 예상할 수 있다. 다만, 단점이라면 곱하는 수가 [2의 멱수의 곱]을 할 때에만 사용할 수 있다는 것이다.

    이것 뿐만이 아니다. 거꾸로 생각하면 나눗셈도 할 수 있다. 다만, 나머지는 버려진다고 생각하면 될 것이다.(한 번 쉬프트한 경우에는 나머지를 볼 수도 있다. 왜그런지는 위의 그림을 보고 각자 생각해 보기 바란다) sar로도 부호없는 정수의 나눗셈은 보여줄 수 있다. sar을 이용하게 되면 부호있는 정수까지도 나눌 수 있다. 왜냐하면 그림 2에서 볼 수 있듯이 최상위 비트(부호비트)를 그대로 유지시켜 주기 때문이다. 예제를 보라.

    mov al, 0F0h ; AL = 1111 0000 (-16)
    sar al, 1 ; AL = 1111 1000 (-8), CF = 0

    위에 보이듯이, 오른쪽 산술 쉬프트 1번은 2로 나누는 효과가 있다. sar의 두번째 피연산자를 2로 한다면 4로 나누는 효과가 있다. 이것도 역시 2의 멱수에 대해서만 나눗셈을 할 수 있음에 유의하라. 전혀 새로운 것이 아니다. 10진수에서도 10의 멱수로 곱하거나 10의 멱수로 나누는 경우는 계산이 한층 더 쉽지 않은가? 2진수니까 2의 멱수로 곱하거나 2의 멱수로 나누는 것이 쉬울 뿐이다.

    div를 이용한 나눗셈의 경우에는 오버플로우 발생 시 직접 알려주지만, 쉬프트를 이용했을 경우에는 오버플로우가 발생해도 그냥 이상한 값을 지닌 채 계속 진행하게 되므로 조심해야 한다.

  • ROL, ROR, RCL, RCR

    그림을 보면 바로 이해할 수 있으며, 사용방법또한 쉬프트와 같다.


    << 그림 3 : ROL, ROR >>


    << 그림 4 : RCL, RCR >>


    쉬프트를 제대로 이해했다면 위의 네 그림도 잘 이해할 수 있을 것이다. 사용 형식은 쉬프트와 완전히 같으며, 실제 그 기능도 매우 미세한 차이가 존재함을 알 수 있다. 간단한 활용 예를 들자면, 상위비트들과 하위비트들을 교환하는데 rol이 쓰인다는 것을 들 수 있다.

    mov al, 26h
    rol al, 4 ; AL = 62h

    주석에 적은 16진수에서 볼 수 있듯이, AL의 상위 4비트와 하위 4비트가 바뀌었다.
  • =============================================================================================
     
  • 이진수 곱셈

    이진수의 곱셈을 할 때 쉬프트를 이용할 수 있다는 사실은 앞에서 배웠다. 그러나 곱하는 수가 2의 멱수가 아니라도 곱셈에 쉬프트를 이용할 수 있다. 원리는 매우 간단하다.
        EAX * 36 = EAX * (32 + 4)
    = (EAX * 32) + (EAX * 4)
    36은 2의 멱수가 아니다. 하지만 2의 멱수들의 합으로 나타낼 수 있고, 분배법칙을 이용해 풀어내면, 두 개의 항이 나온다. 각각은 EAX에 2의 멱수를 곱한 것이다. 만약 EAX에 123이라는 값이 들어있다고 가정해 보자.

                    0 1 1 1 1 0 1 1      123
    * 0 0 1 0 0 1 0 0 36
    --------------------
    0 1 1 1 1 0 1 1 0 0 123 SHL 2
    + 0 1 1 1 1 0 1 1 0 0 0 0 0 123 SHL 5
    ------------------------------
    0 0 0 1 0 0 0 1 0 1 0 0 1 1 0 0 4428

    이 곱셈 과정을 보면 정확히 쉬프트를 함으로써 어떤 일이 일어나는지 알 수 있다. 또, 곱하는 수를 2의 멱수들의 합으로 나타낸다는 것도 어떤 일을 하게 되는 것인지 알 수 있을 것이다. 그렇다면 직접 코딩을 해보자

    .code
        mov eax, 123
        mov ebx, eax
        shl eax, 5
        shl ebx, 2
        add eax, ebx

  • 비트 추출해 내기

    종종 바이트나 워드는 한 개 이상의 정보를 갖기도 한다. 각각의 정보는 비트 스트링으로 되어있다. 예를 들어, MS-DOS에는 dx에 날짜를 리턴해 주는 한 함수가 있다. 그 함수는 다음과 같이 구성되어있다.


    << 그림 5 : DX에 들어간 날짜정보>>

    보다시피 년도는 7비트에 걸쳐서, 월은 4비트에 거쳐서, 날짜는 5비트에 걸쳐서 있다. 이제 여기서 년도와 월, 일을 읽어내려면 일정한 길이의 비트열을 추출해 내야 한다. 어떻게 하면 될까?

    먼저 '일'을 빼와보자. 먼저, 원본을 손상시키게 되면 나중에 불편해 질 수 있으므로, dl의 사본을 복사 한 후('일'을 빼내므로 DH는 필요없다), 5~7비트를 비워주면 0~4비트만 남게 되어 날짜만 남는다. 이 과정은 다음과 같은 코드로 수행될 수 있다.
            mov al, dl           ; 사본을 al로 복사한다.
    and al, 00011111b ; 비트 5~7을 0으로 만든다.
    mov day, al ; day 에 저장한다.

    그 다음 비트 5~8을 빼내려면 어떻게 해야 할까? 중간에 있는 거라 조금 고민될 지 모른다. 먼저 '월'이 최하위비트에 있도록 쉬프트 한 다음에 아까와 같이 불필요한 비트들을 비워주면(0으로 만들어 주면) 된다. 다음 코드를 보라.
            mov ax, dx           ; ax에 사본을 복사한다.
    shr ax, 5 ; 오른쪽으로 5번 쉬프트한다.
    and al, 00001111b ; 비트 4~7을 비워준다.
    mov month, al ; month에 저장한다.

    마지막으로 year를 다루는 것은 이제 쉽게 이해가 될 것이다. dh만 다루면 된다. 이젠 코드를 보는 것이 더 이해가 빠를지 모르겠다.
            mov al, dh           ; al에 복사본을 만든다.
    shr al, 1 ; 오른쪽으로 1번 쉬프트한다.
    mov ah, 0 ; ah를 0으로 만든다.
    add ax, 1980 ; 1980에 상대적인 년도이다.
    mov year, ax ; year에 저장.

    MS-DOS에서는 1980년으로부터 몇년이 지난지를 년도값으로 갖고 있다. 따라서 소스코드에서 1980을 더해준 것임을 잊지 않길 바란다.

  • 마치는 글
    이번 회에서는 주로 쉬프트와 곱셈, 나눗셈에 대해 알아보았다. 어려운 내용은 없었던 듯 하다. 쉬프트와 같은 것은 잘만 사용하면 매우 유용할 수 있다. 다음회에는 매우 흥미로운 내용을 배우게 된다. 실제 런타임 스택에서 함수에 대한 정보가 어떻게 저장되는지, 지역변수가 어떻게 사용되는지, 파라미터는 어떻게 스택에 존재하는지 등등을 공부해 볼 것이다. 다음회는 정말로 기대해도 좋다고 장담한다. 그럼 다음회에서는 좀더 열의에 찬 모습으로 마주앉아 본 강좌를 진행할 수 있었으면 한다.
  •  
    ARTICLE
  • 시작하기에 앞서...

    이제 본 강좌도 절반을 넘어섰다. 시작이 반이라는 말이 있다. 하지만 나는 역으로, 절반이 시작이라는 말을 하고 싶다. 이럴 때일수록 쉽게 풀어져 버리기 십상이기 때문이다. 절반이 지난 만큼, 처음 시작할 때의 마음가짐 그대로 차근차근 강좌를 써 나가려 한다.

    이번 회의 제목은 "정수 산술 연산(이진연산)" 이다. 제목을 보고는 감이 잘 오지 않을 것이다. 산술 연산에는 덧셈과 뺄셈도 포함되지만, 이전에 덧셈과 뺄셈을 배운 바 있기 때문에, 곱셈과 나눗셈, 나머지구하는 연산에 대해 알아볼 것이다. 또 요긴하게 쓰일 수 있는 쉬프트 연산에 대해서도 배울 것이다.

  • MUL 인스트럭션

    MUL은 multiply의 약자로 부호없는 정수에 곱하기 연산을 할 때 사용한다. 곱하기를 하려면 피연산자가 몇 개가 필요할까? add인스트럭션과 비슷한 형식이라면, 피연산자가 두 개 필요할 것이다. 하지만 MUL은 피연산자를 하나만 받는다. 그렇다면 피연산자 하나는 어디로 갔을까?
      (곱해지는 수) * (곱하는 수) = (결과)
    MUL인스트럭션에서 (곱해지는 수)를 읽어올 레지스터와 (결과)를 저장할 레지스터가 미리 정해져 있다. 이는 마치 loop인스트럭션이 루프 카운터로 ecx를 이용하는 것과 같다.(이렇게 읽어올 레지스터가 미리 정해져 있는 것이 IA-32의 특징이라고 여러 번 설명했다.) 따라서 mul은 피연산자를 (곱하는 수)하나만 받는 것이다. 단, 여기서 (곱해지는 수)와 (결과)가 저장될 자료의 크기는 (곱하는 수)의 크기에 따라 결정된다. 방금 말한, 미리 정해진 레지스터는 표1을 보면 알수 있다.


    표를 가만히 살펴보자. (곱해지는 수)의 자료 크기는 (곱하는 수)의 자료 크기와 같다. (결과)는 (곱하는 수)의 자료 크기의 두 배 큰 곳에 저장이 된다. 곱하기한 결과가 곱하는 자료의 크기보다 더 큰 경우를 처리하기 위해서 이다. DX:AX, EDX:EAX와 같이 표기한 것은 결과값이 두 곳에 나눠 들어간다는 뜻이다. :(콜론)의 왼쪽은 상위비트이고, 오른쪽은 하위 비트이다. 만약 결과값에서 상위 절반 비트가 0이 아니라면 Carry Flag가 1이된다. 예를 들어, 16bit 피연산자에 ax를 곱했을 때, 곱은 DX:AX에 저장되는데, dx가 0이 아니면 Carry Flag는 1이된다.

    한가지만 덧붙여 이야기 하겠다. 표1에서 (곱하는 수)가 16bit인 경우 왜 EAX에 저장하지 않고 굳이 둘로 나누어 dx와 ax에 나누어 저장할까? IA-32이전에의 16bit프로세서에서는 extended 레지스터가 없었다. 즉 eax, ebx, ecx, edx가 없었다는 이야기다. 따라서 결과값을 dx와 ax에 나누어 저장해야만 했었다. 만약 이를 IA-32아키텍쳐로 오면서 eax에 저장하도록 한다면 이전에 만들어진 프로그램과 호환이 되지 않게 되며, CISC계열의 장점을 해치게 된다. 따라서 이전의 방식을 그대로 사용하는 것이다. 이렇듯 인스트럭션에도 CPU발전의 역사가 숨어있음을 알아둔다면, 좀더 이해가 빠를 것이다.

    참고로 부호있는 정수의 곱셈에 대해서는 imul을 사용해야 한다. 연산의 과정에 대한 설명은 생락하겠다. 결과값은 여전히 2의 보수를 이용하여 음수를 나타내게 되므로 크게 다른 것은 없다.

  • DIV 인스트럭션

    부호없는 정수의 나누기를 할 때 쓰이는 인스트럭션이고, divide의 약자이다. 나누기에 대해 잠시 생각해 보자. 곱하기와 달리, 나눗셈을 할 때에는 나머지가 나온다.
      (나눠지는 수, 피제수) = (나누는 수, 제수) * (몫) + (나머지)
    사용형식은 mul과 마찬가지로 피연산자를 하나만 받는다. 그것은 나누는 수 이다. 그렇다면, (나눠지는 수)는 미리 어딘가에 넣어두었어야 하고, (몫)과 (나머지)는 연산 후에 레지스터 어딘가로 들어갈 것이다.


    표 2에 나와있는대로, (몫)과 (나머지)가 해당 레지스터로 들어가게 된다. 따라서 우리는 div인스트럭션을 사용하려면 eax레지스터나 edx레지스터에 값을 미리 채워놓아야 한다. 단, 조심할 것은 이 인스트럭션을 수행한 후에 플래그들이 어떻게 바뀌어 있을지는 정의되어있지 않다는 점이다. 또한, 나눗셈을 한 후 (몫)이 들어갈 수 있는 레지스터의 크기보다 큰 경우가 발생하는데,(이를 오버플로우라 한다) 이럴 경우 진행되던 프로그램이 바로 종료되어버리므로 이러한 상황이 발생하지 않도록 조심해야 한다.

    참고로, 나누기 연산에서 동시에 (몫)과 (나머지)가 나오므로 C에서 정수에 대한 /(나누기)연산과 %(나머지)는 결국 같은 인스트럭션을 수행하는 것임을 알 수 있을 것이다.

  • 산술 예제

    프로그래밍 언어에서 자주 사용되는 간단한 수식을 어셈블리어로 변환해 보겠다. 아래의 예는 C에서 사용될 수 있는 수식이다.
      var4 = (var1 + var2) * var3;
    var1 ~ var4는 각각 4byte의 부호없는 정수형이라고 가정하자.
  • 다음과 같이 어셈블리어로 코딩할 수 있다.

        mov eax, var1
         add eax, var2
         mul var3
         jc tooBig
         mov var4, eax
         jmp next
    tooBig:
         ; 에러처리.....
    next:


    tooBig으로 점프한 것이 에러처리를 위해서라는 것은 쉽게 알 수 있을 것이다.