ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 시스템프로그래밍 04 - 어셈블리어 조금 더 맛뵈기02
    쾌락없는 책임 (공부)/시스템프로그래밍 2021. 4. 19. 20:14
    반응형

    본 포스트는 강동완 교수님의 '시스템프로그래밍' 강의를 듣고

    이해한 것들을 정리한 포스트입니다.

    - 강의자료는 올리지 않습니다


    <어셈블리어에서 사용하는 숫자와 관련한 각종 연산들>

    이것들이 하는 역할은 메모리에서 레지스터로 데이터를 이동하는게 주요 역할이며 형식은 아래와 같습니다.

    (라벨) Nemonic  (Operands - 최대 3개까지 가능)

     Operands는 아는대로 피연산자이며 최대 3개까지 가능, 맨 처음 들어온 Operands가 목적지가 됩니다. 

     

    Direct Memory Operands

     흔히 볼 수 있는 변수의 형태라고 할 수 있습니다. CPU의 입장에서는 어차피 변수명을 offset, 기계어로 번역된 주소로 인식하게 됩니다.

     + [] 으로 감쌀 수 있습니다. (in MASM)

     

    MOV

    mov  destination, source

     source에 있는 값을 destination으로 이동시키게 됩니다. 몇가지 규칙들이 있는데

     

     + 두 피연산자가 메모리가 될 수는 없습니다. (하나 이상의 레지스터가 있어야 된다는 말)

     + 두 피연산자는 같은 사이즈여야 한다

     + 피연산자로 Instruction Pointer 레지스터를 destination으로 사용 불가

     

    로 위 규칙들만 잘 지킨다면 나머지는 모두 허용해 줍니다.

    ( 메모리 to 메모리가 안되니 실제로 고수준 언어에서의 swap은 레지스터를 이용하고 있었던 것입니다. )

     

    + 서로 다른 사이즈를 연산하기 위해서는 Overlapping이 필요합니다

    one BYTE 12h
    two WORD 1234h
    
    mov eax, 0	 	    ; eax 00000000h
    mov al, one		    ; eax 00000012h
    mov ax, two		    ; eax 00001234h
    mov eax, 12345678h  ; eax 12345678h
    mov ax, 0000h		; eax 12340000h;

     이런 식으로 레지스터의 크기를 이용해서 다른 사이즈를 연산할 수 있게 됩니다.  ( EAX >절반> AX  >절반>  AH, AL )

     

    MOVZX / MOVSX  (feat. 인텔 엔지니어들...)

     위 방법으로 서로 다른 크기의 값들을 연산할 수 있게 해주는 친구들입니다! 이것들이 나오게 된 이유는 작은 값을 큰 레지스터에 넣게 될 때 부호가 걸리기 때문입니다. 작은 메모리에 있는 값이 음수인지, 양수인지에 따라 큰곳으로 이동하면 값이 달라지게 되고 이걸 프로그래머가 미리 생각을 한 뒤 연산을 진행해야 했습니다. 그런데 다행히 작은 값의 부호에 맞게 선택을 하면 컴퓨터가 알아서 연산을 해주게 됩니다.

     

    - MOVZX : 앞에 0을 채워주게 됩니다. 따라서 양수용이라고 할 수 있습니다.

    - MOVSX : 전달받은 소스의 앞자리로 앞을 채우게 됩니다.

    one BYTE 11001100b
    two BYTE 10001010b
    three BYTE 0000001b
    
    MOVZX ax, three	; ax = 0000 0000 0000 0001
    MOVZX ax, one	; ax = 0000 0000 1100 1100
    MOVSX ax, two	; ax = 1111 1111 1000 1010
    MOVSX ax, three ; ax = 0000 0000 0000 0001

     생각을 해 보는데 MOVSX로 하면 되지 않나? MOVZX의 사용처가 굉장히 애매하다고 생각을 했습니다.

     

    LAHF / SAHF

     LAHF는 Flag레지스터에 있는 값을 AH레지스터로 옮겨주는 일을 해줍니다. 반대로 SAHF는 AH 레지스터에 있는 값을 플래그 레지스터로 이동시킵니다. 이때 사용되는 플래그 레지스터라 함은 EFLAGS로 이 레지스터의 하위 1바이트만을 이용합니다.

     EFLAGS의 하위 1바이트에는 부호, zero등의 각종 정보들이 들어있습니다. (아래 자세하게 다루게 됩니다)

    flagtemp BYTE ?
    
    lafh
    mov flagtemp, ah	; flagtemp에 저장
    safh				; ah에 있는 값을 다시 플래그로

     

    XCHG

    두 피연산자의 정보를 바꿔주는 역할을 합니다. 기존에 알고 있는 swap와 같은 기능을 한다고 볼 수 있습니다. 사용법은 mov와 비슷하며 역시 메모리 to 메모리는 불가하다는 특징이 있습니다. 그리고 두 피연산자의 크기는 같아야 합니다)

    (메모리 to 메모리를 하기 위해서는 임의의 변수를 만들고 mov를 통해 XCHG가 가능합니다)

     

    Direct-offset Operands

    arr BYTE 11, 12, 13, 14, 15...
    
    mov al, arr		; al = 11
    mov al, [arr+1] ; al = 12
    mov al, [arr+3] ; al = 14

     어셈블리어에서 arr은 어셈블러에게 주소값으로 인식이 됩니다. 이 주소값을 기준으로 차례대로 뒤 숫자들이 위치해 있으며 +1, +2 연산을 추가해 주소값을 이동함으로서 접근이 가능합니다.

     [ ] 의 경우 쓰지 않아도 되지만 가독성을 위해 사용하는게 거의 디폴트라고 합니다. 또한 위의 경우 바이트라서 +1이지만 WORD의 경우 +2, +4, DWORD의 경우 +4, +8처럼 크기를 고려해서 작업을 해야 합니다.

     

     

    각종 산술 연산들

    1. INC / DEC

     순서대로 ++, -- 를 해주는 작업으로 이 작업의 결과에 따라 FLAG레지스터(Carry 레지스터 제외)들이 영향을 받게 됩니다.

     

    2. ADD / SUB

     C 언어에서 += / -= 과 같은 역할을 하며 피연산자로 destination, source를 받고 있습니다.

     피연산자를 받아들이는건 mov와 같아 메모리 to 메모리가 불가능하단 점, 연산 결과로 FLAG레지스터들이 영향을 받는다는게 있습니다.

     

    3. NEG

     Negative의 약자라고 볼 수 있으며 two's complement를 이용해서 양수를 음수로, 음수를 양수로 바꿔주는 역할을 하고 있습니다. 어차피 기계어로 번역되면 다 숫자가 되니 보수를 취할 수 있는 것입니다. 그리고 이 연산을 통해서도 FLAG 레지스터들이 영향을 받게 됩니다.

     

    * 플래그 레지스터가 뭔데 영향을 받니 마니 하고 있냐

    플래그 레지스터들은 프로그래머가 연산 결과에 대해서 다양한 상태를 알고 싶을때 사용할 수 있는 레지스터로 CPU에게 부가 명령(if문 같은거)으로 사용되기도 합니다.

    종류 설명
    Carry (CF) unsigned 인 수에게 오버플로우가 일어났는지     (   (-) + (-) 가 + 가 되었는지   )
    Overflow (OF) signed인 수에게 오버플로우가 일어났는지   (   (+) + (+) 가 - 가 되었는지   )
    Zero (ZF) 연산 결과로 인해서 수의 비트들이 전부 0이 될 때 1이 되는 플래그
    Sign (SF) 결과의 최상위 비트(MSB)와 동일한 값
    Parity (PF) 최하위 바이트에서 1인 비트가 짝수개 있으면 1, 홀수개면 0
    Auxility Carry (AC) 연산 결과에서 최하위 바이트의 3번째에서 carry가 발생했는지

    Auxility Carry의 경우 이전 BCD로 수를 표현했을 때의 관습으로 크게 기억할 필요가 없다고 말씀하셨습니다. 또 CF와 OF에서 음수인지 양수인지는 전부 사람의 입장이고 컴퓨터가 신경쓰는 부분은 아닙니다.

     

     - CF의 경우 0이 아닌 수를 NEG하면 +1이 됩니다.

          ex) sub al, 2에서 al에 1이 있다면  0000 0001 + 1111 1110이라 carry는 없는 것 같지만 2가 2의 보수가 되었으니 CF 는 1이 됩니다.

                반대로 al에 3이 있다면 0000 0011 + 1111 1110 이라 carry가 나왔지만 2의 보수 과정에서 CF=1이 되었으니 +1을 해서 CF=0이 됩니다.

     - OF 의 경우 signed인 수에서 +와 +를 더했는데 -가 나온다던가, - 와 -를 더했는데 +가 된 경우 1이 됩니다.

        한마디로 범위를 벗어날 경우 켜지는 플래그라고 생각하면 됩니다.

        NEG 연산에서 -128같은 경우 범위가 -128~127인 경우 OF가 1로 켜지게 됩니다.

     

     

     

    <어셈블리어에서 사용하는 데이터와 관련한 각종 Directives/Operatiors>

     

    1. Directives

    Offset

     해당 데이터가 메모리의 시작 부분부터 얼마나 떨어져 있는가, 한마디로 주소값이라고 보면 됩니다. 참고로 프로세서마다 주소값이 다르긴 하지만 지금 배우는 어셈블리어는 x86프로세서의 32비트 프로세서를 가정하고 있으니 4바이트로 생각하고 있습니다. (크기에 주의)

    .data
    arr BYTE 10, 20, 30, 40,...
    .code
    mov esi, OFFSET arr+3	; 40의 주소가 들어가게 됩니다

    DWORD(4바이트)의 변수를 이용하면 포인터처럼 사용이 가능합니다.

    .data
    arr1 DWORD 100 DUP(?)
    pArr1 DWORD arr1

     레이블에 다른 레이블을 위에처럼 넣으면 변수화된 포인터처럼 사용이 가능하다.

     

    ALIGN

     지정한 값대로 변수들을 정렬해 줍니다. 디폴트는 1이며 이 값에 따라 변수들 간 빈 공간을 창출해 데이터 끊김을 방지합니다.

    val1 BYTE ? ; 주소값 : 0
    ALIGN 2
    val2 BYTE ? ; 주소값 : 2 (원래라면 1이 되어야 한다)
    val3 BYTE ? ; 주소값 : 4

    Label

     변수명 같은게 아니라 코드상에 LABEL로 되어 있는 것들을 의미합니다. 이것들은 저장공간을 할당하지 않고 size attribute를 라벨에 삽입하게 됩니다. 이건 아래 표를 보는게 이해가 더 빠를 듯 합니다.

    .data
    labelex LABEL DWORD
    val1 WORD 5678h
    val2 WORD 1234h
    .code
    mov eax, labelex	; eax : 12345678h
    labelex가 여기서부터 DWORD만큼 위치를 가리킴 val1 78
      56
      val2 34
      12

     이 LABEL은 메모리 공간을 차지하지 않는다는게 아주 특징적입니다. 


    2. Operators

    PTR

    원래의 피연산자의 크기를 Override, 한마디로 데이터의 크기를 변경하는 연산자라고 할 수 있습니다.

    78
    56
    34
    12

     데이터가 위 처럼 들어가 있는 var DWORD 1234567h 가 있다고 합시다. 이곳에

    mov ax, DWORD

     를 하면 크기가 맞지 않아 오류가 나게 됩니다. 대신 PTR을 아래와 같이 이용하면

    mov ax, WORD PTR var

    이런식으로 오류 없이 사용할 수 있게 됩니다. 대신 ax에는 WORD만큼의 크기인 5678이 들어가게 됩니다. (리틀엔디안) 또한 위 예시처럼 큰 값을 작은 곳으로 뿐 아니라 작은 값을 큰 값으로 이동할 수 있습니다. (대신 메모리 근처에 뭐가 있는지 잘 알아야 합니다)

     

    TYPE / LENGTHOF / SIZEOF

     단독으로 사용할 수 없습니다 (return 값이라서).

    TYPE : 해당 데이터가 몇 바이트인지 알려줍니다.

    LENGTHOF : 한 크기 선언에 몇개의 원소들이 있는지 원소들의 갯수를 말해줍니다.  (아래 코드 참고)

    SIZEOF : TYPE x LENGTHOF 의 값을 return 합니다.

    .data
    var1 BYTE ?
    var2 WORD ?
    var3 DWORD ?
    
    arr1 WORD 1, 2, 3
    arr2 WORD 30 DUP(?), ?
    
    str1 BYTE "1234", 1
    
    arr3 BYTE 1, 2, 3, 4
    	 BYTE 5, 6, 7, 8
    arr4 BYTE 1, 2, 3, 4,
    		  5, 6, 7, 8
              
    
    ; TYPE var1 : return 1, TYPE var2 : return 2, TYPE var3 : return 4
    ; LENGTHOF arr1 : return 3, LENGTHOF arr2 : return 31
    
    ; LENGTHOF는 ,로 구분한다고 보면 된다
    ; LENGTHOF arr3 : return 4, LENGTHOF arr4 : return 8

    <Indirect Addressing - 간접 주소 지정>

     Direct offset은 앞에서 한대로 라벨+숫자 같은 형태로 한걸 의미합니다. 그런데 실제로는 이걸 많이 사용하지 않고 특정 레지스터를 C의 포인터처럼 사용해 그 값을 조작하는 방식인 Indirect Addressing을 많이 쓴다고 합니다.

     

    32비트의 Protected 모드 (16비트 호환을 위한 모드) 에서

    이 모드에서는 32비트 사이즈의 레지스터를  [ ] 로 감싸면서 사용할 수 있습니다. 만약 [esi]라고 하면 esi가 가리키고 있는 값을 의미하게 됩니다.

    .data
    val1 BYTE 12h
    .code
    mov esi, OFFSET val1
    mov al, [esi]	; al = 12h;
    
    mov [esi], bl ; esi가 가리키는 주소로 가서 bl의 값을 넣게 된다

     이렇게 사용하는 경우 inc [esi] 이런식으로 쓸 수 없습니다. 왜냐면 저 주소에 있는 값의 크기를 모르기 때문입니다. 대신 inc BYTE PTR [esi] 와 같은 방식으로는 사용이 가능합니다.

     

    배열과 함께 사용하는 간접 주소

    .data
    arr BYTE 10, 20, 30, ...
    .code
    mov esi, OFFSET arr
    mov al, [esi]	; AL = 10
    inc esi
    mov al, [esi]	; AL = 20
    inc esi
    mov al, [esi]	; AL = 30

     이런식으로 inc를 이용해서 하나씩 주소값을 증가하면서 볼 수 있습니다. 대신 위는 BYTE가 기준이기 때문에 inc를 한거지 WORD를 쓴 경우 add 2같은 식으로 해줘야 합니다.

     

    + Indexed Operands : constant(=label)를 레지스터에 널어 유효주소로 만드는 것으로 조금 더 고수준 언어의 인덱스와 비슷한 형태라고 할 수 있습니다.

    .data
    arr BYTE 10h, 20h, 30h
    .code
    mov esi, OFFSET arr
    mov ax, arr[esi]	; ax에는 10h가 담기게 된다
    mov ax, arr[esi+1]  ; ax에는 20h가 담기게 된다, +1은 데이터 크기마다 달라집니다
    mov eax, 3 * TYPE arr ; 이런 식으로도 가능

     위 TYPE를 사용하면 조금 더 코딩하는 느낌이 들기는 하지만 인텔에서 자비를 조금 더 베풀어서 scale factor를 사용해서 좀 더 인덱스같이 쓸 수 있게 해주었습니다.

    .data
    arr DWORD 1, 2, 3, 4, 5...
    .code
    mov esi, 3
    mov eax, arr[esi*4]	; 이러면 3번째 값인 4가 들어가게 됩니다.
    mov eax, arr[esi*TYPE arr]	; 이런식으로 사용 가능

     

     

    포인터

    다른 변수의 주소를 저장하는 변수로 프로세서에 따라 크기가 달라지게 됩니다. 아래 코드는 32비트 기준입니다.

    .data
    arr BYTE 1, 2, 3, 4...
    ptr DWORD arr 		 ; arr의 offset
    ptr DWORD OFFSET arr ; 이런 식으로 명확하게도 가능

    + TYPEDEF를 통해서 built-in 타입을 통해서 새로운 타입을 생성 가능 (C++에도 있다)

    PBYTE TYPEDEF PTR BYTE	; BYTE를 가리키는 포인터 타입, DWORD와 크기가 같다
    
    arr BYTE 10, 20, 30
    ptr1 PBYTE ?
    ptr2 PBYTE arr
    반응형

    댓글

Designed by Tistory.