IT_Programming/Assembly

[전광성의 어셈블리어 이해하기] 구조체와 매크로

JJun ™ 2007. 7. 2. 10:44
LONG
 
 
  • UNION 사용하기

    UNION은 흔히 공용체라고 부르기도 한다. 구조체와 생김새는 비슷하지만 한 가지 다른 점은 모든 멤머가 같은 메모리 공간을 사용한다는 것이다. 즉, UNION에 세 개의 필드가있다고 할 때, 이 세 가지 중 한가지만 사용할 수 있다는 것이다. 따라서 UNION의 크기는 세 필드중 사이즈가 가장 큰 필드의 크기와 같다. 다음과 같이 정의하면 된다.

        유니온이름 UNION
            필드들...
        유니온이름 ENDS
    

    또한 구조체의 필드중 몇몇의 필드만 묶어서 UNION으로 정의할 수 있다.

        구조체이름 STRUCT
            구조체필드들...
            UNION 유니온필드이름
                유니온필드들...
            ENDS
        구조체이름 ENDS
    

    약간 복잡해 보이지만 사실은 '유니온필드들'만 유니온으로 감쌌을 뿐이다. 유니온필드에 접근할 때는 '구조체이름.유니온필드이름.유니온필드' 로 접근하면 된다. 지금까지 언급한 특징만 다를 뿐이지, 사용하는 방법에 있어서 유니온과 구조체는 차이점이 없다. 위와같이 하는 것이 헷갈린다면 먼저 유니온을 정의한 후, 구조체 필드에 유니온 변수를 선언하면 된다. 그리고 하는 일은 두 가지 방법이 완전히 같다.

    몇몇 독자는 UNION을 과연 어디에 쓰게 될지 궁금해 하실지 모르겠다. 작성하는 프로그램이 메모리를 매우 적게 차지해야하는 경우, 구조체의 멤버 중 동시에 쓰이지 않는 멤버를 유니온으로 묶어서 사용하면 메모리를 절약할 수 있다. 이와는 달리 유니온만의 특성을 이용한 예가 있다. 바로 IP주소에 대한 것이다. 소켓 프로그래밍을 해보신 분은 알겠지만 IPv4에서 IP주소는 4바이트이다. 우리가 눈으로 볼 때는 이 4바이트의 IP주소를 1바이트씩 잘라 읽어야 편리하다는 것을 잘 알고 있을 것이다. (우리가 잘 알고 있는 203.253.21.151의 형태) 이러한 경우 UNION을 유용하게 써먹을 수 있다.

        IP UNION
            inDword DWORD 0
            inByte  BYTE 0 0 0 0
        IP ENDS
    
    IP.inDword에 4바이트의 IP주소를 넣고, IP.inByte , IP.inByte + 1, IP.inByte + 2, IP.inByte + 3으로 하나씩 값을 읽으면 우리가 읽기 편한 형태가 될 것이다. 다만 여기서 인텔 계열의 CPU는 리틀엔디안 오더를 사용하기에 우리가 흔히 생각하는 것과는 반대의 순서로 읽어야 한다는 점에 유의해야할 것이다.(3회 참고)

  • 매크로란?

    매크로란, 어셈블리 구문들을 하나로 묶어놓은 것이다. 겉으로 보기엔 프로시져와 매우 유사한 형태를 갖고 있다. 하지만 C에서의 매크로 함수와 같아서, 스택에 수많은 push와 pop을 해가며 함수를 호출하는 것이 아니라, 단지 코드가 덧씌워질 뿐이다. 따라서 속도는 매우 빠른 장점을 지닌 반면, 조심해서 사용하지 않으면 코드가 덧씌워지기 때문에 프로그램의 크기가 매우 커질 것이다. 다음과 같은 매크로가 정의되어있다고 하자.

        NewLine MACRO
            call Crlf
        ENDM
    

    이 매크로는 단지 화면에 개행문자(텍스트 에디터에서 엔터를 누른 것과 같은 효과)를 찍는 일만 수행한다. 매크로를 어떻게 정의하는지 대한 정확한 설명은 다음 단락에 이어질 것이다.

        .code
            NewLine
    

    위와 같은 코드는 전처리기(어셈블러가 컴파일하기 전에 수행된다)에 의해 다음과 같이 바뀐다.

        .code
            call Crlf

  • 매크로 정의하기

    매크로는 소스코드의 어느 부분에서도 정의될 수 있다. 하지만 매크로는 소스코드에서 정의되기 이전에 호출될 수는 없다.

        매크로이름 MACRO 파라미터1, 파라미터2, ...
            구문들
        ENDM
    
    정의는 위와 같이 한다. 매크로가 단순히 텍스트 치환보다 좋은 이유는 바로 파라미터를 넘길 수 있다는 점이다. 다만 조심할 점은 파라미터의 값이 매크로 내부로 넘어가는 것이 아니라는 점이다. 매크로는 단지 코드를 복사해주는 일만 하기 때문에, 코드 수준에서 파라미터를 인자로 넘어온 변수이름으로 치환하는 것에 불과하다. 만약 호출 시에 매크로가 요구하는 파라미터 개수보다 적은 개수가 넘어온다면 나머지는 빈칸으로 남겨진다. 이런 경우 대부분 구문 오류가 발생할 것이다.
    다음은 인자로 넘어온 문자를 화면에 찍어주는 일을 하는 매크로이다.

        mPutchar MACRO char
            push eax
            mov al, char
            call WriteChar
            pop eax
        ENDM

  • 매크로 호출하기

    매크로를 호출하려면 다음과 같이 사용한다.

        매크로이름 인자1, 인자2,...
    
    이제 실제로 매크로를 호출한 경우 코드에 어떤 일이 일어나는지 직접 살펴보자. 윗 문단에서 정의한 mPutchar를 예로 들어 설명하겠다.

        mPutchar 'A'
    
    라고 사용한 경우, 컴파일되기 전에 코드는 다음과 같이 변환된다.
        push eax
        mov al, 'A'
        call WriteChar
        pop eax
    
    코드는 위와 같이 변환된다. 다시한번 강조하지만 매크로의 파라미터는 단순한 텍스트 치환이다.

  • LABEL 디렉티브

    사실 LABEL디렉티브는 구조체나 매크로와는 그다지 관련이 있지 않다. 하지만 이 디렉티브를 언급하는 이유는 나중에 나올 링크드 리스트 예제에 이것이 나오기 때문이다. 크게 어려운 내용이 아니니 부담없이 읽어주기 바란다.
    4회에서 나왔던 내용이지만, 다시한번 변수에 대한 이야기를 해 보겠다. .data에 선언된 변수의 이름은 레이블이다. 우리가 흔히 '라벨'이라고 부르는 것을 떠올리면 레이블의 역할이 뭔지 알 수 있을 것이다. 우리가 선언한 데이터들은 데이터 세그먼트에 들어가게 되고 이 데이터 세그먼트의 시작부분으로부터 선언된 데이터가 존재하는 곳 까지의 거리(Offset)를 통해서 그 데이터에 접근하게 된다. 여기서 '거리'라고 하는 것은 숫자이기 때문에 사람이 기억하기가 어렵다. 따라서 각각의 '거리'에 대한 레이블을 하나씩 만들어놓은 것이 데이터 레이블인 것이다.

    LABEL디렉티브는 데이터 세그먼트에 이미 존재하는 변수에 또 하나의 이름과 타입을 지정해주는 데 이용된다. 다음의 사용예를 보아라.

        .data
            val16 LABEL WORD
            val32 DWORD 12345678h
        .code
            mov ax, val16           ; AX = 5678h
            mov dx, val16 + 2       ; DX = 1234h
    
    ax와 dx에 들어간 값이 뒤바뀐것 같다는 생각이 든다면 본 강좌 3회에 있는 리틀엔디안 오더를 다시 보기 바란다.

  •  

     

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

     

     

  • 반복 블럭

    반복 블럭은 어셈블리어 구문들을 여러번 반복하게 해주는 '디렉티브'이다. 이들의 특징은 반복횟수가 상수에 의해 결정된다는 것이다. 이들은 우리가 흔히 아는 반복문과는 약간 다르다. 지금 말하는 반복 블럭은 일종의 매크로와 같은 역할을 하는 것임을 깊이 새기기 바란다.
    반복 블럭에는 WHILE, REPEAT, FOR, FORC 등이 있는데, 대표적으로 숫자에 적용되어 가장 자주 쓰이는 REPEAT에 대해서 알아보자.

  • REPEAT 디렉티브

    다른 반복 디렉티브들은 수식, 심볼, 문자에 대해 반복하는 반면, REPEAT 디렉티브는 상수수식의 값만큼 반복하기 때문에, 그만큼 쓰임새가 크다.

        REPEAT 상수수식
            구문들...
        ENDM
    직접 써보지 않으면 느낌이 오지 않을 것이다. 아래와 같은 코드가 어떤 일을하게 될지 추측해 보라.

        .data
        array LABEL DWORD
        iVal = 10
        REPEAT 100
            DWORD iVal
            iVal = iVal + 10
        ENDM
    위와같은 코드는 루프를 100번 돌면서 다음과 같은 배열을 만들 것이다.

        array DWORD 10, 20, 30, 40, ... 1000
    
    원소의 사이즈는 4바이트이고 총 100개의 원소를 갖게 된다. 따라서 REPEAT디렉티브는 많은 변수를 선언하고 초기화할 때 유용하다. 다시한번 말하지만 반복블럭은 일종의 매크로이며 반복회수만큼 코드를 붙여넣는다는 것을 잊지 않기 바란다.

  • $ 연산자

    이 연산자는 조금은 특이한 연산자이다. 예제를 먼저 살펴보자.

        list WORD 10, 20, 30, 40
        ListSize = ($ - list) / 2      ; ListSIze는 4
    
    list의 사이즈를 구하는데 $연산자를 이용하였다. $는 이 연산자가 쓰인 위치의 오프셋을 리턴해준다. 쉽게 이야기하면 $가 쓰일 자리에 변수가 정의되었다면 그 변수가 갖게 되었을 '데이터 세그먼트 시작부분으로부터의 거리(Offset)'를 리턴해 준다는 뜻이다(이 Offset을 흔히 주소라고도 부른다). 쉽게 말하면 $는가장 최근에 정의한 데이터의 바로 다음 주소를 리턴해 준다는 말이다. 위의 예에서 $는 메모리상에서 list 배열 네번째 원소의 바로 뒤를 가리키며, list는 배열의 첫 번째 인덱스를 가리키므로 ($ - list)는 8이 된다. 왜 4가 아니고 8이냐는 궁금증이 생긴다면, list배열이 WORD형으로 되어있음을 다시한번 눈여겨 보시기 바란다.

  • 링크드 리스트 예제

    본회에서 배운 내용을 복습하는 의미에서 링크드리스트를 사용하는 간단한 프로그램을 작성해 보겠다.
    링크드 리스트(Linked List)는 연결리스트라고도 하며, 각각의 노드를 포인터를 이용하여 연결함을로써 구현된다. 그림 2와 같은 구조를 떠올리면 이해하기 쉬울 것이다.(사실 여기까지 강좌를 읽으며 이해하고 계신 분이라면 충분히 알고 있을 것이다.) 그림 2는 단방향의 링크드리스트를 나타낸다.

    <그림 2 : 링크드 리스트(linked list)>
    링크드 리스트를 구현해 보았다면 알겠지만 구조체를 사용해야 한다. 링크드 리스트를 위한 구조체를 정의할 때 반드시 다음 노드의 주소를 갖는 필드를 적어 주어야 함은 말할 밀요도 없다.

    이제 구조체를 정의해 보자. 구조체의 정의는 너무나도 간단하다.

        ListNode STRUCT
            NodeData DWORD ?     ; 노드의 데이터값을 갖는다.
            NodePtr  DWORD ?     ; 다음 노드의 주소를 갖는다.
        ListNode ENDS
    
    한 회 내에서 데이터 추상화된 링크드 리스트를 만드는 것은 무리이므로, 최대한 간단히 다음과 같이 링크드리스트를 미리 만들어 놓겠다. 이것은 물론 .data에 들어갈 코드이다.

        TotalNodeCount = 15
        NULL = 0
        Counter = 0
        .data
            LinkedList LABEL DWORD           ; (1)
            REPEAT TotalNodeCount            ; (2)
            Counter = Counter + 1            ; (3)
            ListNode <Counter, ($ + (Counter * SIZEOF ListNode))>
            ENDM
            ListNode <0, NULL>                  ; (4) 꼬리노드
    
    첫번째 줄에서 세번째 줄 까지는 단지 기호상수의 정의에 불과하므로 .data이하부터 설명하겠다. 이곳은 헷갈리기 쉬우니 눈여겨 보기 바란다.
    (1) 먼저 LABEL디렉티브가 나왔는데, LABEL디렉티브 뒤에 나온 DWORD는 형식적인 것이다. 왜냐하면 나중에 사용할 때는 어차피 ListNode인 것처럼 사용하기 때문이다. LinkedList라는 데이터 레이블은 링크드리스트 첫 번째 노드에 대한 데이터 레이블로 쓰이게 된다.
    (2) REPEAT디렉티브는 첫 번째 인자의 상수값만큼(여기서는 15) 루프를 돌리게 된다.
    (3) 따라서 Count가 1씩 증가되면서 15개의 ListNode가 선언된다. 위 코드에서는 선언과 동시에 초기화를 시켜주고 있는데, 두 번째 필드에서는 ($ + (Counter * SIZEOF ListNode))을 이용하여 다음 노드의 오프셋을 얻어온다. $값은 LinkedList레이블이 가리키는 오프셋을 갖고, REPEAT블럭 내에서 변하지 않는다. 왜냐하면 루프가 돌기 이전에 $연산자가 계산되기 때문이다.
    (3) 결과적으로, 꼬리노드를 포함한 16개의 노드가 모두 연결된다.

    이제 본격적으로 main프로시져를 정의해 보겠다. 다음의 코드는 데이터 영역에서 만든 링크드 리스트를 탐색하며 각각의 NodeData를 찍고난 후 종료한다.

        .code
        main PROC
            ; (1)
            mov esi, OFFSET LinkedList
            ; NodeData 필드의 모든 정수들을 보여준다.
        NextNode:
            ; (2)꼬리노드인지 체크해 본다.
            mov eax, (ListNode PTR [esi]) .NextPtr
            cmp eax, NULL
            je quit
            ; (3)NodeData를 출력한다.
            mov eax, (ListNode PTR [esi]) .NodeData 
            call WriteDec
            call Crlf
            ; (4)포인터를 다음 노드로 옮긴다.
            mov esi, (ListNode PTR [esi]) .NextPtr
            jmp NextNode
        quit:
            exit
        main ENDP
        END main
    
    (1) 링크드 리스트의 시작 주소를 esi에 넣어둔다. 이제 esi는 노드를 가리키는 포인터로 쓰일 것이다.
    (2) 루프를 계속 돌 것인지를 결정하는 부분이다. esi가 가리키는 주소에 존재하는 ListNode형 구조체변수의 NextPtr이라는 필드를 읽어서 eax에 넣는다. 물론 eax에 넣는 이유는 NULL인지 판단하기 위해서다. NextPtr필드가 NULL이라는 것은 더이상 다음 노드가 없다는 뜻이기 때문이다. 만약 NULL이라면 quit레이블로 점프하여 프로그램을 종료하게 된다.(NULL은 데이터 영역에서 정의한 기호상수이며 0과 같다.)
    (3) esi가 가리키는 주소에 존재하는 ListNode형 구조체변수의 NodeData필드의 값을 eax에 읽어들인 뒤, 해당 데이터를 출력하고 개행문자를 출력한다. 개행문자는 우리가 텍스트 에디터에서 엔터를 누를 때와 같은 효과를 내는 문자이다.
    (4) 현재 esi가 가리키는 ListNode형 구조체변수의 NextPtr필드 값을 다시 esi에 복사해 넣는다. 따라서 esi는 다음 노드의 주소를 갖게 될 것이다. 그 다음 다시 NextNode레이블로 돌아가서 지금까지 해온 과정((2)~(4))을 반복한다.

  • 강좌를 마치며...

    이번 회에서는 구조체와 매크로에 대해 배워보았다. 적어도 한 가지의 고급언어를 프로그래밍해보신 여러분들은 어셈블리어를 보면서 자신이 다뤘던 프로그래밍 언어들이 어떻게해서 어셈블리어로 구현될지에 대해서도 머릿속에 조금씩 떠오르리라 믿는다. 다음 회에서는 32비트 윈도우즈 프로그래밍을 간단히 배워볼 것이다. 그러나 직접 어셈블리어로 윈도우 프로그래밍을 하는 것은 단지 창 하나 띄워보는 정도로 그치고, IA-32의 메모리 관리방법에 좀더 중점을 두고 집필할 예정이다. 이쪽을 배우는 것이 좀더 여러분에게 도움이 될 것 같아서이다. 다음 회에는 좀더 흥미로운 주제로 여러분에게 다가갈 생각이다. 그럼 이만 줄이겠다.

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

    어느덧 본 강좌도 끝을 향해 가고 있다. 이제 우리는 최소한 어셈블리어 코드를 보고 어떤 일을 하는지 정도는 파악할 수 있지만, 아직 뭔가 허전함을 느낄지 모르겠다. 본 회에 배울 구조체와 매크로에 관련된 내용은 그러한 허전함을 채워주리라 믿어 의심치 않는다.

    다른 언어도 마찬가지이지만, 나중에 버전업이 되어 새로운 것이 나오더라도 예전에 사용하던 형식과 개념 등은 그대로 묻어있게 마련이다. 어셈블리어도 마찬가지인데, 특히나 다른 프로그래밍언어에서 흔히 쓰이는 것이 어셈블리어에 약간 원초적(?)인 모습으로 존재하는 것을 볼 수 있다. 여기서 원초적이라는 말은 좀더 사용하기 불편하고 원시적으로 생겼다는 이야기이다. 필자는 어셈블리어의 프로시져나 배열, 구조체, 매크로 등을 처음 접할 때도 이러한 사실을 느끼고는 매우 흥미로워 했던 기억이 난다. 여러분도 기존에 사용하던 언어에 존재하던 클래스나 구조체, 매크로 등이 어셈블리어에 좀더 원초적(?)으로 존재하는 것을 배우며 희열을 느껴보길 바란다.

  • 구조체란?

    다른 프로그래밍 언어를 접해보신 분이라면 구조체가 무엇인지는 잘 알고 계실 것이다. 그래도 형식상 간단히 설명하자면 구조체는 개념적으로 연관있는 변수들의 그룹이자 틀이며 데이터형이다. 물론 이렇게 설명한다고 해서 이해하시는 분은 아무도 없을 것이다. 구조체를 하나 간단히 정의해 보겠다.

        COORD STRUCT
            X WORD ?
            Y WORD ?
        COORD ENDS

    위 구조체의 이름은 COORD이며 이것이 곧 데이터형이 된다. 뒤에 STRUCT라고 적힌 것을 보면 구조체를 말하는 것이라 추리할 수 있다. 또 마지막에 ENDS가 보이는데, 추측해 보건데, END Struct의 약자일 것이다. 보다시피 구조체의 정의는 너무나 간단하다. 구조체에 포함된 하나하나의 변수는 필드(field)라 불리우며, 멤버변수라고도 부른다. 그리고 ?표시를 한 곳은 초기화를 하지 않겠다는 뜻이다.
    구조체에 관련되어 배울 항목은 정의, 선언, 구조체변수 참조 이다. 하나하나씩 항목별로 배워보자.

  • 구조체의 정의

    구조체의 정의는 다음과 같이 한다.

        구조체이름 STRUCT
            필드선언들...
        구조체이름 ENDS

    구조체를 정의할 때는 초기값을 같이 넣어줄 수 있다. 초기값을 적는 형식은 우리가 일반 변수를 선언할 때와 같다. 다음의 예를 보자.
        Employee STRUCT
            IdNum    BYTE "000000000"
            LastName BYTE 30 DUP(0)
            Years    WORD 0
            SalaryHistory DWORD 0, 0, 0, 0
        Employee ENDS
    <그림 1 : Employee 구조체>

    첫번째 필드 IdNum을 보자. 스트링을 초기값으로 정해 놓았다. 숫자'0' 9개와 널문자 0, 총 10바이트가 들어간다. 잘 이해가 가지 않으면 3회를 다시 참고하기 바란다. 두번째 필드 LastName은 바이트원소가 30개 들어가는 배열에 초기값으로 모두 0을 넣어 두었다.(비어있는 스트링과 같기 때문에 그림1에서는 (null)이라고 적어 놓았다) 세번째 필드 Years는 WORD형으로 0을 하나 갖는다. 마지막 필드는 SalaryHistory로써 DWORD형 원소 4개에 각각 초기값을 0으로 넣어두었다. 두말할 필요도 없이 초기화를 하고 싶지 않으면 ?를 사용하면 된다.

    한가지 유의할 것은 위에 적어놓은 것은 구조체의 정의라는 것이다. 따라서 일종의 데이터 타입이 하나 생긴 것이고, 이를 사용하려면 구조체 변수를 선언해서 사용해야 한다.

  • 구조체의 선언

    좀 더 엄밀히 말하면 구조체에 대한 인스턴스의 선언 또는 구조체 변수의 선언이다. 두말할 필요도 없이 다음의 예제를 보면 쉽게 이해갈 것이라 믿는다. COORD는 본 회의 "구조체란?"단락에서 정의해 놓았던 그 구조체이다.

        .data
            point1 COORD <5, 10>
            point2 COORD <>
            point3 COORD {}

    이렇게<와 >를 사용하여 초기화를 해준다. 만약 < 와 >안에 아무것도 적어주지 않는다면 각 필드에 구조체의 정의에서 적어놓았던 초기값이 적용된다. <와 > 대신 { 와 }을 사용해도 무방하다. 이제 다음 구문을 보고 무슨 뜻일지 한번 생각해 보자.
        person3 Employee <, "Jones">

    눈치가 빠른 분은 이미 눈치챘을 것이다. 두번째 필드만 프로그래머가 지정한 초기값으로 초기화 하고 나머지는 디폴트 초기값으로 적용시키게 된다. 마지막으로 , 구조체의 배열은 어떻게 사용하는지 고민해 보자.
        AllPoints COORD 3 DUP (<0,0>)
    

    이제 COORD형 구조체 변수가 세 개 생겼고 구조체의 배열로 사용할 수 있게 되었다.
    지금까지는 구조체 변수를 선언하는 방법이었고, 다음 단락에서는 직접 구조체 변수를 사용하는 법을 배워보자.

  • 구조체 변수 사용하기

    구조체 변수를 사용하는 것은 놀랍게도 C언어와 다를 바가 없다. '.'을 이용하여 멤버에 접근하기 때문이다.

        .data
            worker Employee <>
        .code
            mov worker.SalaryHistory, 20000        ; 첫번째 봉급
            mov worker.SalaryHistory + 4, 30000    ; 두번째 봉금
    

    C와 비슷하게 쓴다고 해서 마지막 줄의 코드까지 헷갈리지 않길 바란다. + 4를 했는데도 두번째 봉급이 된 이유는 DWORD타입(4바이트를 차지)이기 때문이다. 또 배열이라고 해서 다른 언어처럼 특별한 접근방식이 존재하는 것이 아니라 단지 주소에 덧셈을 하여 다음 원소로 접근한다는 것도 잊지 않기 바란다.

    이제 인다이렉트 오퍼랜드로 어떻게 접근하는지 알아보자. 인다이렉트 오퍼랜드는 4회에서 언급한 바 있다. 쉽게말하면 포인터로 접근하는 방법이다.


            mov esi, OFFSET worker
            mov ax, (Employee PTR [esi]).Years
    

    [esi].Years로 사용하는 실수를 범하지 않길 바란다. 구조체는 어셈블리에서 매우 잘 정의되어 있으며 사용하는데 있어 크게 어려울 것이 없다는 것을 느꼈을 것이다.

  •