IT_Programming/Assembly

[전광성의 어셈블리어 이해하기] 스트링과 배열

JJun ™ 2007. 7. 2. 10:42
LONG

 

 

  • 스트링 비교 예제

      간단한 예제를 하나 소개하고자 한다. 두 스트링을 비교하는 예제인데, 반드시 두 스트링의 길이가 같아야 한다. 두 스트링 중 첫번째 것이 작으면 "Source is smaller"라는 문자열을 출력하고, 그렇지 않으면 "Source is not smaller"라는 문자열을 출력하게 된다.

          INCLUDE Irvine32.inc
          .data
          source BYTE "INTERNET   "
          dest   BYTE "INTERNETCOM"
          str1   BYTE "Source is smaller", 0dh, 0ah, 0
          str2   BYTE "Source is not smaller" 0dh, 0ah, 0
          .code
          main PROC
              cld
              mov  esi, OFFSET source
              mov  edi, OFFSET dest
              mov  ecx, LENGTHOF source
              repe cmpsb
              jb   source_smaller
              mov edx, OFFSET str2
              jmp done
          source_smaller:
              mov edx, OFFSET str1
          done:
              call WriteString
              exit
          main ENDP
          END main

      대부분 앞에서 설명한 내용이므로 굳이 세세히 설명하지 않아도 될 것 같다. 다만, 비교하다가 중지된 경우 분기를 해준다는 것이 조금 다르다. 공백의 아스키 코드 값 보다 'C'의 아스키 코드 값이 더 크므로 수행시 "Source is smaller"라는 문자열이 출력될 것이다. .data부분에 선언된 것 중 str1과 str2 뒤에 0dh와 0ah는 캐리지리턴과 라인피드로 개행문자이다. WriteString이라는 프로시져를 호출하는 부분이 있는데, 파라미터로 edx에 문자열의 주소를 받는다. 따라서 경우에 따라서 알맞는 스트링의 주소를 edx에 넣어두고 WriteString 프로시져를 호출함으로써 화면에 문자열을 출력할 수 있다. 앞으로도 잘 모르는 프로시져가 나오면 이름과 문맥을 살펴보고 의미를 파악하면 되므로 당황하지 않길 바란다.

    • 나머지 스트링 인스트럭션들

      이제 대충 감을 잡았을 테니 나머지 인스트럭션들은 잡다한 설명을 제외하고 어떻게 쓰는지, 어떤 경우에 사용하면 좋을지를 설명하겠다.

      scasb, scasw, scasd는 scan string의 약자로서 역시 비교를 한다. 하지만 여기서는 esi를 사용하지 않고 al 또는 ax 또는 eax 를 이용하고, 이곳에 있는 값과 edi가 가리키는 곳의 값을 비교하게 된다. 물론 비교 후에는 방향플래그에 따라 edi에 있는 주소값을 증가 또는 감소시킬 것이다.
      이것은 주로 문자열 내에 특정 문자가 있는지 확인할 때, 배열 내에 특정 자료와 일치하는 것이 있는지 확인할 때 편리하게 쓰인다. 특히, 널종료 문자열에서 문자열의 맨 끝으로 이동하려 할 때 유용하게 쓰일 수 있다.

      stosb, stosw, stosd는 store string의 약자로서 자료를 복사하는 역할을 하므로 movs종류와 유사한 일을 한다. 다만 읽어오는 곳이 al 또는 ax 또는 eax이라는 점이 다르다. 여기서도 esi는 쓰이지 않는다. 따라서 al/ax/eax로부터 값을 읽어들여 edi가 가리키는 곳에 쓰게 될 것이다.
      특정 문자로 스트링을 채우려고 할 때 rep접두어와 함께 사용하면 편리할 것이다.

      lodsb, lodsw, lodsd는 load string의 약자로서 자료를 복사하는 역할을 하므로 movs종류와 유사한 일을 한다. 다만 쓰는 곳이 al 또는 ax 또는 eax라는 점이 다르다. 물론 읽어오는 곳은 esi이며, edi를 사용하지 않는다. stos종류와 정 반대의 일을 한다고 생각하면 될 것이다. lods종류는 rep 접두어를 붙여서는 큰 쓸모가 없을 것이다. 왜냐하면 al/ax/eax에 복사되는 자료들이 반복되어봤자 계속해서 덮어 써지기 때문이다. 따라서 이 인스트럭션들은 loop인스트럭션과 함께 사용하는 것이 편리하다.

    • 이차원 배열

      이차원 배열이 무엇인지는 다들 잘 알 것이라 믿는다. 쉽게 말해서 엑셀에 행과 열에 따라 넣은 자료들과 같은 형태라고 생각하면 된다. 그런데 컴퓨터에서 이 이차원 배열을 사용하려면 생각처럼 쉽지 않다. 메모리는 '선형'이기 때문이다. 즉, 메모리에서는 자료를 일렬로 쭉 늘여놓은 형태로 사용한다. 그런데 이것을 마치 행과 열의 평면에서 자료를 다루는 것처럼 사용해야 하니 이것저것 생각할 것이 생기게 되는 것이다. 다음과 같이 자료를 정의해 두었다고 하자.

          .data
          tableB BYTE  10h,  20h,  30h,  40h,  50h
                 BYTE  60h,  70h,  80h,  90h, 0A0h
                 BYTE 0B0h, 0C0h, 0D0h, 0E0h, 0F0h
          NumCols = 5
      

      위와같이 이차원 배열을 정의할 수 있다. 아랫줄의 NumCols는 기호상수로서 이것을 정의한 것 아래에서 NumCols라고 적으면 5로 치환된다. 그렇다면 이차원 배열은 어떻게 사용하면 될까? 다음의 예를 통해서 하나하나 뜯어보자. 그림 1도 참고하면 좀더 이해하기 쉬울 것이다. 아래의 예제는 C에서의 이차원배열로 치자면 tableB[1][2]에 접근하는 예이다.(인덱스는 0부터 시작하는데 주의하라! 일반적으로 행과 열번호에 따라 말하자면 2행 3열이다.)
      RowNumber = 1
      ColumnNumber = 2
      mov ebx, OFFSET tableB
      add ebx, NumCols * RowNumber
      mov esi, ColumnNumber
      mov al, [ebx + esi]
      

      < 그림 1 : 이차원 배열>

      먼저 접근하고 싶은 행과 열을 각각 RowNumber와 ColumnNumber에 넣어두었다. 그리고 ebx에 테이블의 시작주소를 넣었고, ebx에 해당 열까지 옮긴 후(add인스트럭션으로) esi에 접근하고 싶은 열의 숫자를 넣어두었다. 그리고 마지막으로 [ebx + esi]에 접근함으로써 tableB[1][2]에 있는 값을 al로 복사하는데 성공하였다. 코드를 수행한 후에 al에는 80h가 들어가 있을 것이다. 그림1을 참고하면 좀 더 이해가 빠르리라 믿는다.

    • 베이스-인덱스 디스플레이스먼트

      이차원 배열을 다루기 위해 쓰는 두번째 방법이다. 만약 이차원 배열의 시작부분이 table이라고 하면, table[ebx + esi]와 같은 식으로 사용하는 방법이다.(table[ebx + esi]는 [table + ebx + esi]와 같은 의미이다. 이 부분이 이해가지 않으면 4회를 다시 보기 바란다. 물론 반드시 ebx와 esi일 필요는 없으니, 그때그때 비어있는 레지스터를 활용하기 바란다.) ebx에는 '행'과 관련된 정보가 있고, esi에는 '열'과 관련된 정보가 들어있을 것이다. 그런데 어떻게 들어있느냐가 궁금할지 모르겠다. 다음의 예제와 그림 2를 참고하면 좀더 이해가 빠를 것이다.

          .data
          tableB BYTE  10h,  20h,  30h,  40h,  50h
                 BYTE  60h,  70h,  80h,  90h, 0A0h
                 BYTE 0B0h, 0C0h, 0D0h, 0E0h, 0F0h
          NumCols = 5
          .code
              mov ebx, NumCols
              mov esi, 2
              mov al, tableB[ebx + esi]     ; [150 + 5 + 2] = [157]
      

      < 그림 2 : 베이스-인덱스 디스플레이스먼트>

      이차원 배열을 사용하는 첫번째 방법과 다른 점은, 주소 자체를 갖고 있는 것이 아니라, 오프셋만을 계산한 뒤에 배열에 접근한다는 것이다. 물론 인스트럭션 수는 두번째 것이 좀 더 적지만, 첫번째 것이 좀 더 이해하기는 편할 것이다. 각자 취향대로 필요에 따라 사용하길 권한다.

    •  
       
      ============================================================================================
       
       
    • 버블소트

      이번엔 버블소트를 배워보도록 하겠다. C를 할 줄 안다면 버블소트 정도는 잘 알 테지만, 어셈블리어로 어떻게 구현되는지 한번 유심히 살펴보길 바란다.
      버블소팅을 해주는 프로시져를 하나 정의해 보겠다.

      BubbleSort PROC USES eax ecx esi,
          pArray:PTR DWORD,
          Count:DWORD
          mov ecx, Count
          dec ecx
      L1: push ecx
          mov esi, pArray
      L2: mov eax, [esi]
          cmp [esi+4], eax
          jge L3
          xchg eax, [esi+4]
          mov [esi], eax
      L3: add esi, 4
          loop L2
          pop ecx
          loop L1
      L4: ret
      BubbleSort ENDP
      

      < 그림 3 : 버블소트 >

      내부적으로 eax, ecx, esi레지스터를 쓰기에 USES 디렉티브를 사용하여 프로시져 호출 전과 후에 각 레지스터 값이 변하지 않게 하였다. 첫번째 인자로 DWORD형 배열에 대한 주소를 넘겨받고 두번째 인자로 배열 내의 원소수를 넘겨받는다. 물론 호출시 주소를 넘길 때는 ADDR연산자를 사용해야 한다는 것을 잊지 않기 바란다.

      우선 루프가 두개 있음을 진한 글씨를 통해 알 수 있을 것이다. 바깥루프가 한 번 도는 동안 어떤 변화가 일어나는지는 그림3을 참고하면 이해가 빠를 것이다.
      L1레이블 이전까지를 살펴보자. 버블소트에서 바깥 루프는 (원소의 개수 - 1)만큼 돌게 되므로 ecx에 값을 대입시킨다.
      L1레이블은 바깥 루프의 시작부분이다. 여기서 ecx를 스택에 넣는 것은 이중 루프를 돌리기 위함이다. 두 루프가 모두 ecx를 카운터로 사용한다는 점을 염두해 두면 이해할 수 있을 것이다. 그리고 esi에 배열의 시작주소를 넣는다.
      L2레이블은 안쪽 루프의 시작부분으로서, esi가 가리키는 원소와 그 다음 원소를 비교해서 순서가 맞지않을 경우 두 자료를 교환해준다. 만약 순서가 맞아서 교환할 필요가 없다면 L3로 점프하게 된다. 즉, 교환하지 않는다.
      L3레이블에서는 esi가 다음 원소를 가리키게 하고, (여기서 DWORD형이기 때문에 4를 증가시킨 것임을 알아두어라) 아까 스택에 넣어두었던 ecx를 꺼내게 된다. 다시 한번 말하지만, 이는 이중루프를 돌리기 위한 것이다.
      L4레이블은 함수를 종료하는 부분이다. 단지 보기 편하라고 달아둔 것이니 헷갈리지 않기 바란다.

    • 여기서는 버블소트에 대한 알고리즘을 익히고자 한 것이 아니라, 배열과 관련된 내용을 많이 배웠으니 실제 사용할 때 어떻게 쓰이는지 익히고자 하는 의도였다. 따라서 알고리즘이 이해되지 않아도 신경쓸 필요가 없는 것이다. 다만 배열을 저렇게 사용할 수 있구나, 하는 것만 알아두기 바란다.

    •  

    • 마치는 글

      이번 회에서는 조금 특이한 형태의 인스트럭션도 배웠다. CPU내의 인스트럭션 차원에서 배열을 지원해 준다는 것이 어찌보면 의외였을지도 모르겠다. 또, 이차원 배열의 사용법과 버블소트 예제를 배웠는데, 이렇게 하나하나 배워가는 것이 여러분들의 컴퓨터에 대한 이해에 도움을 주었길 빈다. 다음 회에서는 구조체와 매크로를 배운 후, 링크드리스트를 만들어 간단히 테스트하는 예제도 배울 것이다. 구조체를 직접 어셈블리어로 만들어 보는 것도 매우 흥미롭지 않을까 생각한다.다음 회에서 다시 뵙길 빌며 이만 본회를 마치겠다.

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

      이번 회에서는 스트링과 배열을 다룰 때 쓰는 인스트럭션들과 이차원 배열을 사용하는 방법, 문자열 소팅함수를 만드는 방법에 대해 배울 것이다. 또한 본 회의 내용을 이용하면 스트링과 배열 처리를 고급언어를 이용하는 것 보다 훨씬 빠르게 할 수 있다. 어셈블리어에서는 스트링을 다루는 데 이용될 수 있는 좀더 편리한 인스트럭션들이 제공되기 때문이다.(엄밀히 말하면 CPU가 제공하는 것이다.) 이차원 배열은 고급언어에서와 달리 사용하기가 간단하지 않다. 따라서 이를 사용하는 방법을 공부해 볼 것이다. 마지막으로 직접 버블소팅을 이용한 소팅함수를 만들어 볼 계획이다.

    • 스트링관련 인스트럭션들

      스트링 처리와 관련된 인스트럭션들은 다섯가지 '군'으로 분류될 수 있다. 이 다섯가지는 각각 MOVS계열, CMPS계열, SCAS계열, STOS계열, LODS계열 이다. 표 1을 참고하면 각각이 하는 역할을 알 수 있을 것이다. 한가지 알아두어야 할 것은 이들 인스트럭션은 ESI나 EDI를 사용한다는 점이다.

      인스트럭션
      설명
      MOVSB, MOVSW, MOVSD 한 메모리 주소에서 다른곳으로 값을 복사한다.
      CMPSB, CMPSW, CMPSD 메모리에 있는 두 값을 비교한다.
      SCASB, SCASW, SCASD 정수값과 메모리의 값을 비교한다.
      STOSB, STOSW, STOSD 정수값을 메모리에 복사한다.
      LODSB, LODSW, LODSD 메모리의 값을 AL또는 AX 또는 EAX에 읽어들인다.
      < 표 1 : 스트링관련 인스트럭션들>

      스트링 관련 인스트럭션들은 피연산자로 아무것도 받지 않는다. 다만, 사전에 약속해 놓았던 레지스터를 이용하게 될 것이다. 아까 언급했던 esi와 edi는 위의 인스트럭션들이 공통적으로 사용한다. esi에서 s는 source를 뜻하므로 '값을 읽어오는 곳'으로 쓰이고, edi에서 d는 destination을 뜻하므로 '값을 쓰는 곳'으로 쓰인다. 주의할 것은 esi와 edi에 주소가 들어있다는 것이다. esi에서 값을 읽어오는 것이 아니라 esi가 가리키는 곳에서 값을 읽어오고, edi에 값을 쓰는 것이 아니라 edi가 가리키는 곳에 값을 쓴다는 점이다. 따라서 인스트럭션을 수행하기 전에 esi와 edi에 스트링 또는 배열의 시작주소를 넣어둬야 한다.

      위의 표를 보고도 무슨 말인지 잘 모를 수 있다. 뒤에 상세히 설명할 것이니 걱정 말기 바란다. 한가지 궁금한 점이 생길 지 모르겠다. 표 1에 나온 인스트럭션들을 보면 mov인스트럭션과 같이 이전에 배운 것들로도 충분히 할 수 있는 것이기 때문이다. 이에 대한 대답은 rep 접두어(Prefix)에 있다. 다음 단락에서 계속 설명하겠다.

    • REP 접두어

      스트링관련 인스트럭션들은 rep 접두어와 함께 사용될 때 진가를 발휘한다. rep를 '접두어'라 부른 이유는 단독으로 쓰이지 않고 스트링관련 인스트럭션과 결합되어 쓰이기 때문이다. 다음의 표 2에서는 rep, repz, repe, repnz, repne에 대해서 설명하고 있다. repz와 repe는 이름만 다르고 하는 일은 같다. repnz와 repne도 마찬가지이다. 굳이 같은 일을 하는데에 여러 이름이 붙은 것은 단지 코드에서의 가독성을 위한 것이니 신경 쓸 필요는 없을 것이다.

      REP
      ECX > 0 일동안 반복
      REPZ, REPE
      Zero Flag == 0 이고 ECX > 0일동안 반복
      REPNZ, REPNE
      Zero Flag != 0 이고 ECX > 0일동안 반복
      < 표 2 : REP 접두어 >

      접두어를 붙이게 되면 rep 뒤어 적어놓은 인스트럭션을 ecx값 만큼 반복해서 수행한다. 물론 한번 수행할때마다 ecx값을 하나씩 스스로 감소시킬 것이다. 간단한 사용예제는 다음과 같다.

          cld
          mov esi, OFFSET string1
          mov edi, OFFSET string2
          mov ecx, 10
          rep movsb
      < 예제 1 >

      구체적인 설명은 뒤에서 자세히 할 것이니 여기서는 간단히 설명하겠다. 먼저 cld는 방향 플래그(Direction Flag)를 0으로 해준다. 반대로 std는 방향 플래그를 1로 해준다. 방향 플래그에 대해서는 처음 들어보았을 것이다. 이것 역시 뒤에서 자세히 설명할 것이다. 그 다음 세 줄은 인스트럭션이 의도하는대로 수행될 수 있도록 세팅하는 것이다. 특히 마지막 부분의 ecx에 10을 넣었으니 10번 반복할 것이다. 위와같은 코드를 수행하고 나면 string1이라는 문자열에서 10바이트가 string2로 복사되어있을 것이다. 아직 이해가 안 되었으면 다음 단락의 방향 플래그을 계속 읽어보기 바란다.

    • 방향 플래그(Direction Flag)

      스트링 관련 인스트럭션들은 수행 후에 esi와 edi의 값을 증가시키거나 감소시킨다. esi와 edi에는 포인터를 넣어 두어야 한다고 했다. 따라서 esi와 edi는 바로 다음 것을 가리키게 될 것이다. 하지만 이것은 방향플래그가 0일 때 이야기다. 만약 방향플래그가 1일 경우에는 스트링 관련 인스트럭션 수행 후에 esi와 edi의 값이 감소될 것이다. (스트링 관련 인스트럭션이 b로 끝나는 것이면 1바이트 가감, w로 끝나는 것이면 2바이트 가감, d로 끝나는 것이면 4바이트 가감될 것이다. 각각은 Byte, Word, Double word를 의미한다.) 즉, 배열이나 스트링에서 바로 앞을 가리키게 된다. 다음 표에 요약해 놓았다.

      방향플래그의 값
      ESI와 EDI
      설정 인스트럭션
      0
      증가
      CLD
      1
      감소
      STD
      < 표 3 : 방향 플래그(Direction Flag, DF) >

      이렇게 스트링 관련 인스트럭션에서 값을 가감시키므로 예제 1에서와 같이 rep movsb라고 수행했을 때 계속해서 esi와 edi값이 변하여 복사가 제대로 이루어 질 수 있음을 이해할 수 있을 것이다. 즉, movsb가 한 번 수행 되면 esi가 가리키는 곳에서 1바이트를 읽어 edi가 가리키는 곳에 복사한 뒤 esi와 edi를 하나씩 증가시켜 다음 위치를 가리키게 할 것이다. 만약 movsb가 아니라 movsw 였다면 esi와 edi를 각각 2씩 증가시키고, movsd였다면 esi와 edi를 각각 4씩 증가시켰을 것이다. 그래야만 다음 원소를 올바르게 가리키게 되기 때문이다. 참고로, 예제 1에서는 방향 플래그가 0으로 설정되어있기 때문에 esi, edi가 증가한 것이고, 방향 플래그가 1로 설정되어 있을 경우에는 esi와 edi의 값이 감소하게 되므로 조심하길 바란다.

    • MOVSB, MOVSW, MOVSD 인스트럭션

      movsb, movsw, movsd는 예제 1에 대해서 바로 위에 설명해 놓았으니 무엇을 하는 인스트럭션인지는 감이 왔으리라 믿는다. movs는 move string의 약자이고, 뒤에 붙은 b, w, d는 자료의 크기를 말한다. 이들은 esi가 가리키는 곳의 값을 x바이트 읽어서 edi가 가리키는 곳에 복사한다. x바이트는 movsb일경우 1바이트, movsw일 경우 2바이트, movsd일 경우 4바이트가 될 것이다. 그리고는 방향 플래그의 값에 따라 x바이트 만큼 증가 또는 감소하게 된다. 이것이 잘 이해가지 않을 경우 방향플래그 문단을 다시한번 봐주기 바란다.

    • CMPSB, CMPSW, CMPSD 인스트럭션

      cmps는 compare string의 약자이고 뒤에 붙은 b, w, d는 자료의 크기를 말한다. 무엇의 약자인지를 살펴보면 알겠지만, 이 인스트럭션은 두 자료를 비교하게 된다. 비교해서 각종 플래그들을 세팅할 것이다. 따라서 우리는 비교에 따른 분기를 할 수 있다. 물론 이 인스트럭션을 수행하기 전에도 읽어오는 곳의 주소를 esi에 넣어두고, 쓸 곳의 주소를 edi에 넣어두어야 한다. 다음의 예제를 보면 좀더 이해가 빠를 것이다.

          .data
          source DWORD 1234h
          target DWORD 5678h
          .code
              mov esi, OFFSET source
              mov edi, OFFSET target
              cmpsd
              ja L1           ; [esi] > [edi]인 경우
              jmp L2          ; [esi] <= [edi]인 경우
      오랜만에 점프 인스트럭션이 나왔다. 일단 비교 후 'esi가 가리키는 곳의 값'이 'edi가 가리키는 곳의 값' 보다 클 경우 L1이라는 레이블로 점프를 하게 되고, 그렇지 않을 경우 L2라는 레이블로 점프를 하게 된다. 따라서 두 값의 비교를 통한 분기가 가능한 것이다. 그러나, 단지 두 자료를 비교하는 것만으로 cmpsd를 쓰기엔 너무 긴 코드라는 생각이 들지 않는가?

      방금 들었던 예제는 단지 보여주기 위한 것에 불과하다. 정말 쓸모있게 사용하려면 다음과 같이 사용할 수 있다. (movs종류와 마찬가지로 방향플래그의 값에 따라 esi와 edi가 증가 또는 감소한다는 사실을 잊지 않기 바란다)

              mov esi, OFFSET source
              mov edi, OFFSET target
              cld
              mov ecx, count
              repe cmpsd
      
      방향 플래그를 0으로 설정하고 count만큼 cmpsd를 반복하였다. 따라서 순방향으로 증가해가며 비교하되, ecx > 0 이고 비교한 결과가 같을 동안만 반복한다. 따라서 이 예제를 이용하면 문자열이나 배열이 같은지 비교하는 루틴을 만들 수 있을 것이다. 다음 장에서는 좀더 활용적인 예로 설명하겠다.