ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • x86 명령어
    임베디드 SW/어셈블리어 2023. 8. 12. 17:00

    글의 참고

    - https://c9x.me/x86/ 

    - https://stackoverflow.com/questions/29430762/what-is-the-difference-if-any-between-long-and-far-jumps-in-assembly

    - http://unixwiz.net/techtips/x86-jumps.html

    - 325383-sdm-vol-2abcd.pdf [Order Number: 325383-078US]


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - 명령어 읽는 법 

    : 문서를 보다보면 아래의 내용들을 볼 수 있다.

    ADD r/m16, imm16 ; ADD ax, WORD 8
    ADD r32, r/m32 ;  ADD ax, DWORD [address] 

    : imm은 상수값을 의미한다. r/m는 `레지스터 혹은 메모리 레퍼런스`가 온다는 것이다. 물론, 메모리 주소 자체를 사용할 수 도 있다. 그러나, 메모리 주소 자체를 레지스터에 넣는 경우는 생각보다 많지 않다. 뒤쪽에 붙는 숫자들은 사이즈를 의미한다. 앞에 OPCODE가 연산을 할 때, 명시적 작성한 사이즈 만큼만 연산을 한다.

     

     

    - 레이블

    : 레이블에는 현재 로케이션 카운터의 값이 할당된다. 2가지 타입의 레이블이 있다. 

    1" 심볼릭 레이블
    2" 넘버 레이블

     

    : 심볼릭 레이블

    " 심볼릭 레이블은 해당 오브젝트 파일에 하나만 존재해야 한다. 전역적인 스코프를 갖으며, 해당 오브젝트 파일의 심볼 테이블에 등록된다. 심볼릭 레이블은 식별자와 콜론(:) 포맷을 따른다.

    식별자: ; 심볼릭 레이블
    	<코드>

     

    : 로컬 레이블

    " 마침표로 시작하는 레이블을 로컬 레이블이라고 한다. 이 레이블은 오브젝트 파일의 심볼 테이블에는 등록되지 않는다. 그래서 디버깅시에는 보이지 않는다. 로컬 레이블은 자신의 상위 심볼릭 레이블에 포함된다. 아래와 같다.

    1:label1  ; some code 
    2:
    3:.loop 
    4:        ; some more code 
    5:
    6:        jne     .loop 
    7:        ret 
    8:
    9:label2  ; some code 
    10:
    11:.loop 
    12:        ; some more code 
    13:
    14:        jne     .loop 
    15:        ret

    : 위에서 `.loop` 로컬 레이블이 2개다. 그런데, 로컬 레이블을 암묵적으로 사용하면 로컬 레이블의 범위는 자신이 속하는 심볼릭 레이블안에서만 동작하기 때문에 label1.loop의 JNE는 label1.loop를 반복한다. 그리고 label2.loop는 label2.loop를 반복한다. 만약, 아래와 같이 명시적으로 로컬 레이블을 사용할 수도 있다.

     

    1:label1  ; some code 
    2:
    3:.loop 
    4:        ; some more code 
    5:
    6:        jne     .loop 
    7:        ret 
    8:
    9:label2  ; some code 
    10:
    11:.loop 
    12:        ; some more code 
    13:
    14:        jne     local1.loop 
    15:        ret

    : 로컬 레이블을 명시적으로 사용한다면, label2.loop에서 local1.loop로 JMP 하는 것도 가능하다.

     

    : 넘버 레이블

    " `로컬 레이블` 속한다. 그래서 오브젝트 파일의 심볼 테이블에는 등록되지 않는다. 넘버 레이블은 반복해서 재정의가 가능하다. 아래의 예제를 보자.

    1: 1:          / define numeric label "1"
    2: one:        / define symbolic label "one"
    
    / ... assembler code ...
    
    6: jmp   1f    / jump to first numeric label "1" defined
                / after this instruction
                / (this reference is equivalent to label "two")
    
    10: jmp   1b    / jump to last numeric label "1" defined
                / before this instruction
                / (this reference is equivalent to label "one")
    
    14: 1:          / redefine label "1"
    15: two:        / define symbolic label "two"
    
    17: jmp   1b    / jump to last numeric label "1" defined
                / before this instruction
                / (this reference is equivalent to label "two")

     

    : 아래의 내용을 해석하면 결국, `1f`는 가장 가까운 다음 `1:` 심볼릭 레이블을 의미하고, `1b`는 가장 가까운 바로 직전 `1:` 심볼릭 레이블을 의미한다. 넘버 레이블은 주로 루프를 돌릴 때, 많이 사용한다. NASM에서는 넘버 레이블을 지원하지 않는다. GAS(GNU-Assembly)에서는 지원한다.

    1" 6번째 줄 `jmp 1f` -> 14번째 줄 `1:`로 이동
    2" 10번째 줄 `jmp 1b` -> 1번째 줄 `1:`로 이동
    3" 17번째 줄 `jmp 1b` -> 14번째 줄 `1:`로 이동

     

    : x86 어셈블리언어 에서는 프로그램을 종료하는 exit 같은 명령어가 존재하지 않으므로, 해당 라인에서 코드를 더 이상 진행하고 싶지 않다면, `jmp $` 로 현재 라인에서 무한 루프로 진입시켜 그 뒤에 코드를 실행시키지 않을 수 있다.

     

     

    : 데이터 레지스터와 주소 레지스터를 구분해서 사용해야 한다. 아래의 코드를 보자. 

    {
    	BOOT_MSG:    db 'yohdaOS 16-bit Boot Loader', 0
    
    	_start:
                cli
            
                xor ax, ax
                mov si, BOOT_MSG
            
                mov ch, [si+0]
                mov cl, [si+1]
                ...
    }

     

    : 문자열은 당연히 주소로 받기 때문에, 주소 레지스터 계열인 SI 레지스터에 쓴다. 그리고 데이터 계열 레지스터인 CX에 문자를 하나씩 쓴다. 이 때, C언어와 마찬 가지로 주소의 값에 접근하기 위해서는 `[]` 괄호를 이용해야 데이터에 접근할 수 있다. 

     

    : C언어에서 컴파일시 자료형 형변환은 경고 문구로 끝난다. 그러나 어셈블리언어에서는 서로 다른 형변환시에 명확하게 지정하지 않을 경우 에러를 발생시킨다. 서로 사이즈가 맞지 않다는 내용으로 문제가 발생한다.

    1:{
    2:    BOOT_MSG:    db 'yohdaOS 16-bit Boot Loader', 0
    3:
    4:    mov si, BOOT_MSG
    5:
    6:    mov cl, [si]
    7:    mov [es:di], cl
    8:
    9:}

    : 위에서 6,7번 문장은 에러 문장이다. 명확하게 형변환을 작성하지 않았기 때문이다. 1바이트를 2바이트에 집어넣는다는 내용을 작성하려 했으므로, 아래와 같이 수정한다.

     

    1:{
    2:    BOOT_MSG:    db 'yohdaOS 16-bit Boot Loader', 0
    3:
    4:    mov si, BOOT_MSG
    5:
    6:    mov cl, byte [si]
    7:    mov byte [es:di], cl
    8:
    9:}

    : 2바이트를 1바이트로 변경해야 한다는 내용이므로, 2바이트쪽에 형변환 문장을 적용한다.

     

     

    - 함수

    : 함수 작성 및 스택 사용시 주의 사항 1 - 반환값

    " 32비트 x86은 cdecl 함수 호출 규약을 사용한다. 콜러(Caller)에서 매개 변수를 푸쉬한 뒤, 호출이 끝나면 콜러에서 SP를 조정해서 이전에 푸쉬한 파라미터들을 제거해줘야 한다. 그렇면 어셈블리함수를 C언어 호출할 때는 어떨까? 예를 들어, I/O 포트 관련 함수를 어셈블리언어로 만들고 이걸 C언어 함수들이 호출하면 어떻게 될까? 결론만 말하면 C언어에서 함수를 만들어서 어셈블리언어 함수를 호출하면 C 컴파일러가 자동으로 함수 호출 규약을 파악하고 그에 맞게 스택을 정리해준다. 아래의 코드를 보자.

    outb:
        push ebp
        mov ebp, esp
        push dx

        mov dx, word [ebp+8]    
        mov eax, [ebp+12]

        out dx, al
        
        pop dx
        pop ebp 
        ret 8
    outb:
        push ebp
        mov ebp, esp
        push dx

        mov dx, word [ebp+8]    
        mov eax, [ebp+12]

        out dx, al
        
        pop dx
        pop ebp 
        ret

    " cdecl 에서는 위에서 왼쪽 코드와 같이 `ret 8`을 사용하면 안된다. 왜냐면, 지금 위의 코드는 콜리에서 스택을 정리하고 있기 때문이다. 반드시 콜러에서 `add sp, 8` 명령어를 통해 스택을 정리해야 한다. C언어 함수를 사용하면 자동으로 컴파일러가 콜러측에서 스택을 정리하는 코드를 삽입하기 때문에, `ret X` 같은 명령어가 아닌 단일 `ret`만 사용해야 한다.  

     

    : 함수 작성 및 스택 사용시 주의 사항 2 - PUSH 및 POP 명령어

    " C언어에서 어셈블리언어로 작성된 함수를 호출할 때, 주의점이 있다. PUSH 및 POP 명령어는 스택 카운터를 CPU 모드에 따라 이동시키지 않고, `인자`가 어떤 레지스터에 따라 스택 카운터를 이동시킨다. 즉, CPU 모드가 32비트 여도 `PUSH BP` 명령어를 입력하면 스택 카운트는 2 만큼만 움직인다. 아래의 코드를 보자.

    inw:
        push bp
        mov bp, sp                                                                                        
        push dx

        mov dx, word [ebp+8]
        in ax, dx

        pop dx
        pop bp
        ret
    extern u32 inw(const u32 addr1);

    ...
    ...

    u16 pci_get_ven(const struct pci_dev *dev)
    {
        u32 addr = 0;
                                                                                               
        addr = PCI_GET_CONFIG_ADDR(dev->bus, dev->dev, dev->func); 
        addr |= PCI_OFFSET_VEN;

        outd(PCI_CONFIG_ADDR, addr);        

        return (u16)inw(PCI_CONFIG_DATA, 1); 
    }

    " 위의 코드를 실행하는 환경은 x86 보호 모드 환경이라고 전제한다. `inw` 에서 [ebp+8]은 몇일까? 쓰레기값이다. 기대했던 값이 아닌 것이다. 실제 기대했던 값은 [ebp+6]에 들어있다. 이유는 위에서 말했던 것처럼 `PUSH BP` 때문이다.

     

    inw:
        push ebp
        mov ebp, esp                                                                                        
        push edx

        mov dx, word [ebp+8]
        in ax, dx

        pop edx
        pop ebp
        ret
    extern u32 inw(const u32 addr1);

    ...
    ...

    u16 pci_get_ven(const struct pci_dev *dev)
    {
        u32 addr = 0;
                                                                                               
        addr = PCI_GET_CONFIG_ADDR(dev->bus, dev->dev, dev->func); 
        addr |= PCI_OFFSET_VEN;

        outd(PCI_CONFIG_ADDR, addr);        

        return (u16)inw(PCI_CONFIG_DATA, 1); 
    }

    " 2바이트 레지스터를 4바이트 레지스터로 변경했던 예상했던 대로 동작하는 것을 확인할 수 있다. 정리하면, CPU 모드가 바뀌면 해당 모드에서 지원하는 비트에 맞게 레지스터를 사용해야 하는 게 그나마 버그를 덜 발생시키는 방법일 것이라 생각된다.

     

    : 함수 작성 및 스택 사용시 주의 사항 3 - PUSHA[X] 및 POPA[X] 명령어

    " 아래 함수의 반환값은 몇일까? `in eax, edx` 이므로, in 명령어로 받은 값일까? 아니다. 반환값은 PUSHAD 명령어를 통해 들어간 EAX값이 된다. 반환값이 있을 때, PUSHA[X], POPA[X] 계열 함수는 주의해서 사용해야 한다.

    ind:
        push ebp
        mov ebp, esp                                                                                        
        pushad

        mov dx, word [ebp+8]
        in eax, edx

        popad
        pop ebp
        ret

    : 함수 작성 및 스택 사용시 주의 사항 4 - 64비트 PUSH 및 POP 명령어

    " 64비트에서는 PUSH 및 POP 명령어 사용 시, 32비트 레지스터 및 8비트를 인자로 받을 수 없다. 그러나, 16비트 및 64비트는 받을 수 있다. 이 에러가 NASM 어셈블러에서만 발생하고 다른 어셈블러에서는 발생하지 않는 것인지는 아직 확인해보지 못했다. 앞에 링크를 따라가서 확인해보면 알 수 있지만, 많은 사람들이 이상하게 생각하고 있는 에러다. 그래도 일단 명확한 건 각 모드에서는 각 CPU 모드에 맞는 PUSH 및 POP 연산을 해야 한다는 것 같다. PUSH 및 POP은 각 CPU 모드에 최적화되어 있기 때문에(각 모드에 맞게 PUSH 및 POP 시 데이터 정렬이 진행 됨), 32비트에서는 32비트 주소만, 64비트에서는 64비트 주소만 사용해야 한다. 만약, 그렇지 않고 64비트에서 16비트를 사용하면  데이터 정렬이 깨질 가능성이 높다.

     

    " 2B PUSH를 한 번하고, 8B PUSH를 하면 10B가 줄어든다. 64비트 환경에서는 8B 정렬이 기본이다. 지금 앞에 예시는 벌써 정렬이 깨졌다. 그러므로, CPU 모드에 맞게 연산을 해야 한다. 

     

    - 연산 명령어

    : INC

    " inc reg/<mem8, mem16, mem32> 포맷으로 레지스터 혹은 메모리 주소값중 인자를 하나만 갖는다. 그리고 해당 인자의 주소에 들어가있는 값에 +1을 한다. CF 플래그는 그대로 보존한다.

    Destination = Destination + 1

     

    : MUL

    " mul reg/<mem8, mem16, mem32> 포맷으로 레지스터 혹은 메모리 주소값중 인자를 하나만 갖는다. 리고 해당 인자의 주소에 들어가있는 값을 AX와 곱하여 해당 결과값을 AX에 넣는다. 참고로, 이 연산은 FLAG에 영향을 준다. 동작은 아래와 같이 이루어 진다. 1바이트 곱하기면 간단히 AL과 소스를 곱하여 AX에 대입한다. 1바이트가 아니라면 16비트와 32비트로 나눈다. 32비트는 EAX에 저장한다.

    if(IsByteOperation()) AX = AL * Source;
    else { //word or doubleword operation
    	if(OperandSize == 16) DX:AX = AX * Source; //word operation
    	else EDX:EAX = EAX * Source; //doubleword operation
    }

     

    : SUB

    " 형태는 아래와 같다. 목적지와 소스를 빼서 목적지에 대입한다. 

    Destination = Destination - Source;
    sub ax, ab ; ax = ax - bx

     

    : ADD

    "  형태는 아래와 같다. 목적지와 소스를 더해서 목적지에 대입한다. 

    Destination = Destination + Source;
    add ax, bx ; ax = ax + bx

     

    : SHR

    " 오른쪽 쉬프트한다. 형태는 아래와 같다.

    SHR r/m16,imm8 - 예시) shr word [sectors], 6 : sectors 변수를 워드 사이즈안에서 6번 오른쪽으로 쉬프트한다.

     

     

    - 비교 명령어

    : 형태는 아래와 같이 3개가 존재한다. 오퍼랜드 2개를 비교해서 플래그 레지스터의 비트를 SET/CLEAR 한다. 이 `CMP`의 원리는 첫 번째 오퍼랜드와 두 번째 오퍼런드를 `뺄셈`을 통해 이루어진다.

    1" CMP [E]AX, imm8
    2" CMP r/m,imm
    3" CMP r/m,r

     

    : TEST

    " `test` 명령어는 2개의 비연산자를 받아서 `AND` 연산을 수행한다. 그 결과로, `OF`, `CF`는 CLEAR되고 플래그 레지스터의 `SF`, `ZF`, `PF` 비트가 수정된다. 참고로, 결과값은 버려진다. `test` 연산자는 주로 동일 비연산자를 대상으로 자주 사용된다. 예를 들어, `test eax, eax` 와 같이 말이다. 이렇게 하면, 값이 그대로 나온다(주로 `0`을 판단하는데 사용). 여기서 핵심은 `test` 명령어를 실행한 결과로 바뀐 플래그 비트들을 이용해서 분기를 한다는 것이다. 아래의 케이스에서 `test edi, edi` 하고, `jle`가 나오는데 어떻게 될까? `edi`가 0 혹은 음수면 `.L2`로 점프하고, 0보다 크면 브랜치하지 않는다. 참고로, 음수끼리 `&` 연산을 해도 음수다.

    #include <cstdio>

    int main(int argc, char *argv[])
    {
        if (argc > 0) {
            puts ("LOVE\n");
        } else {
            puts ("SOJU\n");
        }
        return 0;
    }
    .LC0:
       .string "LOVE\n"
    .LC1:
        .string "SOJU\n"
    main:
        sub rsp, 8
        test edi, edi
        jle .L2
        mov edi, OFFSET FLAT:.LC0
        call puts
    .L3:
        xor eax, eax
        add rsp, 8
        ret
    .L2:
        mov edi, OFFSET FLAT:.LC1
        call puts
        jmp .L3

     

     

     

    - 이동 관련 명령어

    : FAR JUMP

    " 인텔에서 정의한 `JMP`는 무려 4가지 종류가 있다. 여기서는 x86 모드 전환 시, 필수인 `FAR JUMP` 명령어를 알아본다. `FAR JUMP`는 CS 세그먼트를 바꾸는 명령어다. 일반적인 `JUMP`는 CS를 바꾸지 못하지만, `FAR JUMP`는 CS를 바꿀 수 있도록 설계된 명령어다. NASM 및 ASM에서 `FAR JUMP` 라는 명령이 직접 제공되지는 않는다. 대신, NASM에서는 `jmp 세그먼트:오프셋` , ASM은 `ljmp 세그먼트:오프셋` 형태가 `FAR JUMP`를 의미한다. 아래에서 보다시피, 동일한 권한 레벨에서만 점프가 가능하다.

    ...
    Far jump
    " A jump to an instruction located in a different segment than the current code segment but at the same privilege level, sometimes referred to as an intersegment jump.
    ...

    Far Jumps in Real-Address or Virtual-8086 Mode.
    " When executing a far jump in real-address or virtual-8086 mode, the processor jumps to the code segment and offset specified with the target operand. Here the target operand specifies an absolute far address either directly with a pointer (ptr16:16 or ptr16:32) or indirectly with a memory location (m16:16 or m16:32) ...

    Far Jumps in Protected Mode
    " (The JMP instruction cannot be used to perform inter-privilege-level far jumps.) 
    In protected mode, the processor always uses the segment selector part of the far address to access the corresponding descriptor in the GDT or LDT. The descriptor type (code segment, call gate, task gate, or TSS) and access rights determine the type of jump to be performed ... ... ...
    If the selected descriptor is for a code segment, a far jump to a code segment at the same privilege level is performed. (If the selected code segment is at a different privilege level and the code segment is non-conforming, a general-protection exception is generated.) A far jump to the same privilege level in protected mode is very similar to one carried out in real-address or virtual-8086 mode. The target operand specifies an absolute far address either directly with a pointer (ptr16:16 or ptr16:32) or indirectly with a memory location (m16:16 or m16:32). The operand-size attribute determines the size of the offset (16 or 32 bits) in the far address. The new code segment selector and its descriptor are loaded into CS register, and the offset from the instruction is loaded into the EIP register.

    - 참고 :  Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z [ JMP—Jump ]

    " 그리고 `target operand`라고 있는데, 오프셋을 의미한다. 즉, 어디로 점프할 것인지를 적으면 된다. 이 주소는 논리, 선형, 물리 주소중에 어떤 주소일까? `FAR JUMP`의 `target operand`는 RAM에 로딩되어 있는 물리 주소를 작성해야 한다.

     

    " x86에서 점프의 종류는 3가지다. SHORT JUMP, LONG JUMP, FAR JUMP 점프가 있다. 그 중에서 FAR JUMP는 모드를 전환에 필수적으로 사용된다. 인텔에서 말하는 FAR JUMP는 세그먼트가 바뀌는 JUMP를 의미한다. 그래서 FAR JUMP라 하면 대게는 포맷이 `JMP [SEGMENT:OFFSET]` 으로 사용된다. SHORT JUMP나 LONG JUMP는 오프셋만을 작성하여 점프하는 경우가 많다.

    명령어 설명 플래그 코드
    JE, JZ   ZF = 1  
    JNE, JNZ   ZF = 0  
    JL      
    JLE      
    JNL      
    JG      
    JC      
    JS      
    JO      
    JB `Jump if below`의 약자이다. 0 혹은 양의 정수를 비교할 때 사용된다. `JL`과 기능적으로 동일하지만, JL은 음수도 받는다. CF = 1  

     

     

    : RET

    " 현재 [E|R]SP가 가리키는 곳(on the top of the stack)으로 복귀한다. 인자를 하나 받을 수 있는데, 전달된 파라미터들의 총 사이즈를 명시한다. 그렇면, 복귀시에 자동으로 POP 해준다.

    Transfers program control to a return address located on the top of the stack. The address is usually placed on the stack by a CALL instruction, and the return is made to the instruction that follows the CALL instruction.

    The optional source operand specifies the number of stack bytes to be released after the return address is popped; the default is none. This operand can be used to release parameters from the stack that were passed to the called procedure and are no longer needed.

     

     

    - 메모리 관련 명령어

    : PUSHAD

    " 모든 32비트 범용 레지스터들을 스택에 푸쉬한다. 순서는 다음과 같다. 반환값이 있을 경우, 이 명령어 사용에 신중해야 한다. 32비트 x86은 반환값은 EAX 레지스터에 넣게 되는데 PUSHAD 명령어는 값을 반환하기전에 EAX 레지스터 값을 스택에 푸쉬되기 이전값으로 돌려버리기 때문에 조심할 필요가 있다.

    if(OperandSize = 32) { //PUSHAD instruction
    	Temporary = ESP;
    	Push(EAX);
    	Push(ECX);
    	Push(EDX);
    	Push(EBX);
    	Push(Temporary);
    	Push(EBP);
    	Push(ESI);
    	Push(EDI);
    }
    else { //OperandSize = 16, PUSHA instruction
    	Temporary = SP;
    	Push(AX);
    	Push(CX);
    	Push(DX);
    	Push(BX);
    	Push(Temporary);
    	Push(BP);
    	Push(SI);
    	Push(DI);
    }

    : PUSHA

    " 모든 16비트 범용 레지스터들을 스택에 푸쉬한다. 순서는 `AX, CX, DX, BX, SP, BP, SI, DI` 이다. 여기서 SP도 스택에 들어가게 되는데, 이 때 SP의 값은 PUSHA 명령어가 수행되기 전의 값이 들어간다. 이 명령어도 반환값이 있는 함수에서 사용을 주의해야 한다.

     

    : 참고로, x86_64에서는 PUSHA 및 POPA 계열의 명령어가 존재하지 않는다. 즉, 각각의 레지스터를 개별적으로 PUSH 및 POP 해야 한다.

     

    : MOVS[B|W|D]

    " 형태는 아래와 같다. 오퍼랜드 2개 모두 상수(imm)는 안된다. 주소를 갖는 변수여야 한다. [DS:[E]SI] 에서 [ES:[E]DI] 로 데이터를 복사한다. 이 명령어와 `REP` 명령어를 같이 사용하면 속도가 좋다고 한다.

    MOVS m, m

    " 접두사에 자료형을 붙인 명령어를 사용하기도 한다. 이 명령어들은 오퍼랜드를 직접적으로 받지 않는다. `B`는 바이트, `W`는 워드, `D`는 더블워드를 의미한다. 모두, [DS:[E]SI] 에서 [ES:[E]DI] 로 데이터를 복사한다.

    MOVSB
    MOVSW
    MOVSD

     

    : LEA

    " `LEA` 명령어는 `MOV` 명령어와 많이 비교되는 명령어인데, 이 명령어를 왜 쓰는지 부터 알아야 한다. 아래의 코드를 보자.

    " MOV EAX, EBX+4

    " MOV는 기본적으로 레지스터에 수학적 연산을 통한 주소를 반환하지 못한다. NASM에서 위의 문법은 오류를 발생시킨다. 왜냐면, 레지스터 주소에 특정 연산을 수행하기 때문이다. 위의 연산은 결국 아래와 같이 수정해야 정상 동작한다.

     

    " ADD EBX, 4
    " MOV EAX, EBX

    " `MOV`를 이용하면 주소 연산이 이렇게 복잡해진다. 이제 이 내용을 `LEA`를 사용해보자.

     

    " LEA EAX, [EBX+4]

    " LEA를 이용하면 위와 같이 간단하게 바뀐다. 위의 내용은 `EBX+4`의 값을 반환하는게 아니라, EBX+4의 주소를 반환한다. 즉, LEA를 이용하면 복잡한 연산을 통해 만들어진 주소를 생성할 수 있다.

     

     

    : REP/REPE/REPZ/REPNE/REPNZ

    " 특정 명령어들의 앞에 붙어서 반복해서 실행한다. 예를 들어, `REP` 명령어는 `string insturction`에 속하는 `INS`, `OUTS`, `MOVS`, `LODS`, `STOS` 명령어앞에 붙어서 해당 명령어들을 카운터 레지스터에 명시된 횟수만큼 반복한다. 그런데 주의할 점이 있다. 

     

    " `REPE/REPZ/REPNE/REPNZ`명령어들은 `ZF` 플래그의 값으로 반복을 멈출 수 도 있다는 것이다. 무조건, `카운트 레지스터`만 보고 반복 종료시점을 판단하면 안된다.

    Repeats a string instruction the number of times specified in the count register or until the indicated condition of the ZF flag is no longer met. The REP (repeat), REPE (repeat while equal), REPNE (repeat while not equal), REPZ (repeat while zero), and REPNZ (repeat while not zero) mnemonics are prefixes that can be added to one of the string instructions. The REP prefix can be added to the INS, OUTS, MOVS, LODS, and STOS instructions, and the REPE, REPNE, REPZ, and REPNZ prefixes can be added to the CMPS and SCAS instructions. (The REPZ and REPNZ prefixes are synonymous forms of the REPE and REPNE prefixes, respectively.) The F3H prefix is defined for the following instructions and undefined for the rest

    • F3H as REP/REPE/REPZ for string and input/output instruction.
    • F3H is a mandatory prefix for POPCNT, LZCNT, and ADOX.

    The REP prefixes apply only to one string instruction at a time. To repeat a block of instructions, use the LOOP instruction or another looping construct. All of these repeat prefixes cause the associated instruction to be repeated until the count in register is decremented to 0.

    Repeat Prefix Termination Condition 1 Termination Condition 2
    REP RCX or (E)CX = 0 None
    REPE/REPZ RCX or (E)CX = 0 ZF = 0
    REPNE/REPNZ RCX or (E)CX = 0 ZF = 1

    NOTES:
    * Count register is CX, ECX or RCX by default, depending on attributes of the operating modes.

    - 참고 :  Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z [ REP/REPE/REPZ/REPNE/REPNZ—Repeat String Operation Prefix ]

     

     

    : STOS/STOSB/STOSW/STOSD/STOSQ

    " AL, AX, EAX 레지스터의 값을 ES:EDI 혹은 ES:DI 값에 쓴다. 예를 들어, STOSB 명령어를 사용하면 자동 AL 레지스터가 선택되고 AL 레지스터의 내용이 `ES:DI`가 가리키는 주소에 쓴다. STOSD 명령어를 사용하면 자동으로 EAX 레지스터가 선택되고, `ES:EDI`가 가리키는 주소에 쓴다. 

     

    " AL, AX, EAX 레지스터의 값이 ES:DI 혹은 ES:EDI 에 써진 후, [E]DI 레지스터의 값은 증가 혹은 감소한다. 증가 및 감소의 기준은 EFLAGS 레지스터의 `DL`비트값에 따라 달라진다. `DL`이 1이면, [E]DI의 값은 감소한다. `DI`가 0이면, [E]DI의 값은 증가한다.

    In non-64-bit and default 64-bit mode; stores a byte, word, or doubleword from the AL, AX, or EAX register (respectively) into the destination operand. The destination operand is a memory location, the address of which is read from either the ES:EDI or ES:DI register (depending on the address-size attribute of the instruction and the mode of operation). The ES segment cannot be overridden with a segment override prefix.
    ...

    ...
    The no-operands form provides “short forms” of the byte, word, doubleword, and quadword versions of the STOS instructions. Here also ES:(E)DI is assumed to be the destination operand and AL, AX, or EAX is assumed to be the source operand. The size of the destination and source operands is selected by the mnemonic: STOSB (byte read from register AL), STOSW (word from AX), STOSD (doubleword from EAX).

    After the byte, word, or doubleword is transferred from the register to the memory location, the (E)DI register is incremented or decremented according to the setting of the DF flag in the EFLAGS register. If the DF flag is 0, the register is incremented; if the DF flag is 1, the register is decremented (the register is incremented or decremented by 1 for byte operations, by 2 for word operations, by 4 for doubleword operations).

    - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z [ STOS/STOSB/STOSW/STOSD/STOSQ—Store String ]

     

    : XCHG(Exchange Register/Memory with Register)

    " `XCHG` 명령어는 좋은 점이 다른 명령어들과는 다르게, `LOCK 접두사`를 작성하지 않아도 된다.

    ....
    The XCHG instruction always asserts the LOCK# signal regardless of the presence or absence of the LOCK prefix.
    ....

    - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z

    .... 
    Exchanges the contents of the destination (first) and source (second) operands. The operands can be two generalpurpose registers or a register and a memory location. If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the `LOCK prefix` or of the value of the IOPL. (See the LOCK prefix description in this chapter for more information on the locking protocol.)

    This instruction is useful for implementing semaphores or similar data structures for process synchronization. (See “Bus Locking” in Chapter 9 of the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, for more information on bus locking.)
    ....

    - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z [ XCHG—Exchange Register/Memory With Register ]

     

    : LOCK

    " `LOCK` 명령어는 대개 다른 명령어의 접두사 형태로 쓰이며, 다른 명령어의 목적지 오퍼랜드가 메모리일 때  의미가 있다. 즉, 목적지 오퍼랜드가 레지스터라면 `LOCK`의 의미가 없다. 왜냐면, `LOCK`은 프로세서 내부 버스가 아니라, 시스템 버스에 걸리는 것이기 때문이다.

    Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signal is asserted.

    In most IA-32 and all Intel 64 processors, locking may occur without the LOCK# signal being asserted. See the “IA32 Architecture Compatibility” section below for more details.

    The LOCK prefix can be prepended only to the following instructions and only to those forms of the instructions where the destination operand is a memory operand: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG. If the LOCK prefix is used with one of these instructions and the source operand is a memory operand, an undefined opcode exception (#UD) may be generated. An undefined opcode exception will also be generated if the LOCK prefix is used with any instruction not in the above list. The XCHG instruction always asserts the LOCK# signal regardless of the presence or absence of the LOCK prefix.

    The LOCK prefix is typically used with the BTS instruction to perform a read-modify-write operation on a memory location in shared memory environment.

    The integrity of the LOCK prefix is not affected by the alignment of the memory field. Memory locking is observed for arbitrarily misaligned fields.

    This instruction’s operation is the same in non-64-bit modes and 64-bit mode.
    ....

    - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z

     

    : BTS

    " `bts` 명령어는 특정 비트를 SET 해주는 명령어다. 포맷은 2개의 오퍼랜드를 받는다. 첫 번째 오퍼랜드는 몇 번째 비트를 수정할 것인지를 나타낸다. 두 번재 오퍼랜드는 대상이 된다. 예를 들어, `bts $8, %eax`일 경우, `eax` 레지스터가 가지고 있는 값에서 8번째 비트를 1로 설정하는 것이다. 그리고, SET 되기전에 비트는 ELFAGS 레지스터의 CF 비트에 저장된다. 

    Selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset operand (second operand), stores the value of the bit in the CF flag, and sets the selected bit in the bit string to 1. The bit base operand can be a register or a memory location; the bit offset operand can be a register or an immediate value:
    ...

    - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z [ BTS—Bit Test and Set ]

     

    - 전역 변수 

    : 아래의 예제로 전역 변수의 사용을 살펴보자. 아래의 예제는 NASM 문법으로 작성된 예제다.

    section .text
       global _start          
    	
    _start:                   
        mov eax, choice		  
        add [eax], 1        ; choice 4
        add [eax], 3        ; choice 7
        
        mov [ebx], [choice] ; [ebx] = 7
    
    section .data
    choice: db 3

    : 데이터 섹션 영역에 choice 라는 변수를 선언하고, 3으로 초기화한다. 그리고 _start 심볼에서는 choice 변수의 주소값을 eax 레지스터에 넣어 choice 변수의 값을 변경하고 있다.

     

     

    - 구조체

    : x86 어셈블리어에서 구조체는 거의 배열과 동일하게 사용된다. 그러나, 중요한 차이가 있다. 

    1" 모든 데이터들이 동일한 사이즈가 될 필요가 없다.
    2" 일반적으로 구조체 필드들은 4바이트 정렬된다. 즉, 연속적이지 않고, 중간에 패딩이 존재할 수 있다.
    3" 각각의 구조체 필드는 자신만의 별도의 오프셋을 가질 수 있다.

    : 아래는 C 코드로 작성된 구조체다.

     

    struct MyStruct2
     {
        long value1;
        short value2;
        long value3;
     }

    : 이 코드를 정렬된 구조체와 정렬되지 않은 구조체로 해서, x86 기반의 어셈블리 코드로 바꾸면 다음과 같다. 참고로, ebx에는 `MyStruct2`의 베이스 어드레스가 들어있다고 가정한다.

     

     ;data is 32-bit aligned
     [ebx + 0] ;value1
     [ebx + 4] ;value2
     [ebx + 8] ;value3
     ;data is "packed"
     [ebx + 0] ;value1
     [ebx + 4] ;value2
     [ebx + 6] ;value3

    : 왼쪽은 4바이트 정렬로 된 구조체를 보여준다. `[ebs+6]`은 전혀 사용되지 않는다고 보면 된다. 배열과 크게 다를 바가 없다. 오른쪽이 실제 우리가 표면적으로 보는 구조체와 비슷한 형태다. 각 데이터 사이즈만큼 접근을 하는것을 볼 수 있다. 그러나, 실제로는 컴파일러는 빌드시 구조체들은 왼쪽과 같이 구조체 필드안에 가장 큰 데이터 사이즈로 정렬시켜 버린다.

     

    - 캐스 캐이딩

    : 주의 사항 

    " 캐스 캐이딩시에 자신이 의도한대로 적합한 값을 계산하려면 각 변수의 사이즈 맞는 캐스캐이딩을 진행해야 한다.

    mov [cylins], cx
    and word [cylins], 0xFFC0
    shr word [cylins], 6
    inc word [cylins]

    0x7cf0 <cylins>:        0x00014f24
    0x7cf0 <cylins>:        0x00014f00
    0x7cf0 <cylins>:        0x0001013c
    0x7cf0 <cylins>:        0x0001013d

    : 위에서 핵심적으로 볼 부분은 뒤쪽 2바이트 부분이다. 오른쪽 쉬프트 연산을 했지만, 파란색 1은 오른쪽으로 움직이지 않는 것을 볼 수 있다. 이것은 쉬프트 연산시에 `WORD` 사이즈로 캐스캐스딩을 진행해서이다. 만약, 사이즈를 `DWORD` 로 바꾸면 저 1도 오른쪽 쉬프트가 되버린다.

     

    : DOWRD로 바꾼후 값이 완전히 바뀐 것을 확인할 수 있다.

    and word [cylins], 0xFFC0
    shr dword [cylins], 6
    0x00014f00
    0x0000053c

     

    - I/O 명령어

    : IN

    " 형태는 아래와 같다. 특정 메모리 및 DX 레지스터에 작성된 I/O 포트 주소에서 읽어온 값을 AX 계열 레지스터에 쓴다. I/O 포트 주소의 범위가 0 ~ 65,535 이므로, 2바이트를 저장하는 DX 레지스터면 모든 I/O 포트 주소를 표현할 수 있다.

    IN AL,imm8
    IN AX,imm8
    IN EAX,imm8
    IN AL,DX
    IN AX,DX
    IN EAX,DX

     

     

    : OUT

    " 형태는 아래와 같다. AX 계열 레지스터에 있는 값을 특정 메모리 및 DX 레지스터에 저장된 I/O 포트 주소에 쓴다는 것이다. I/O 포트 주소의 범위가 0 ~ 65,535 이므로, 2바이트를 저장하는 DX 레지스터면 모든 I/O 포트 주소를 표현할 수 있다.

    OUT imm8, AL
    OUT imm8, AX	
    OUT imm8, EAX	
    OUT DX, AL	
    OUT DX, AX	
    OUT DX, EAX

    : src는 AL(1B), AX(2B), EAX(4B)가 올 수 있다. 자신이 I/O 포트에 쓰고 싶은 바이트에 맞게 레지스터의 크기를 선택하면 된다.

     

     

    : OUTS/OUTSB/OUTSW/OUTSD

    " 형태는 아래와 같다. 대개 OUT 명령어는 DX 레지스터 관련이 깊다. 그래서 I/O 포트의 주소 범위는 2바이트 내에 있다(0B ~ 65,535B). OUTS는 DX:(E)SI에 저장된 값을 DX에 저장된 I/O 포트 주소에 쓴다는 뜻이다. 

    OUTS DX, m8
    OUTS DX, m16	
    OUTS DX, m32	
    OUTSB	
    OUTSW	
    OUTSD

    : OUTSB/OUTSW/OUTSD 는 암묵적으로 목적지는 DX에 명시된 I/O 포트 주소롤 설정하고, DS:(E)SI 에 값을 DX에 쓴다. 여기서 B는 바이트(1B), W는 워드(2B), D는 더블워드(4B)가 된다. 

     

    : INVLPG

    " 인자로 메모리 주소를 받는다. 인자로 전달딘 주소를 포함하고 있는 페이지 하나를 TLB 엔트리에서 제거(Invalidate or Flush)한다. 즉, TBL 엔트리에서 해당 주소를 포함하는 페이지가 있을 경우, 프로세서는 해당 TBL 엔트리를 제거한다. 이 명령어는 인자로 전달된 주소를 포함하는 하나의 페이지만을 타게팅하는 용도로 쓰이지만, 특정 상황에서 이 명령어는 모든 TBL를 제거하기도 한다. `상위 절반 커널`에서 사용된다.

    invlpg mem

     

     

    - 플래그 명령어

    : cld

    " cld는 주로 x86 System V Calling Convention 때문에 사용되는 명령어다. 플래그 레지스터에 10번째 비트인 DF는 `Direction`을 의미하는데, 문자열 증감 방향과 관계되어 있다. cld(DF=0)는 문자열의 처리 방향을 증가하는 형식으로 설정한다. 즉, 첫 번째 문자열부터 처리를 한다는 뜻이다. std(DF=1)라는 명령어가 있는데, 이 명령어는 마지막 문자부터 처리를 시작한다. i386 System V Calling Convention 에서는 함수 진입 전 `DF=0` 임을 보장한다. 그러므로, 함수를 호출하기 전 `cld` 를 굳이 호출하지 않아도 된다.

     

    : cli

    " 인터럽트 플래그는 인터럽트의 발생 여부를 알려준다. 즉, 인터럽트 플래그가 SET되면, 프로세서는 인터럽트를 받게된다. `cli` 명령어는 인터럽트 플래그를 클리어한다. 즉, 인터럽트를 허용하지 않는다. 인터럽트 플래그가 관리하는 인터럽트는 하드웨어 인터럽트다. 그래서 소프트웨어 인터럽트는 계속 받게된다. NMI 계속해서 받게 된다.

     

     

    - Nop 명령어

    : 각 CPU 아키텍처마다 `NOP`, `no-op`, `NOOP` 이라고 하는 `no operation` 형태의 명령어를 제공한다. 이 명령어는 각 아키텍처에 맞게 여러 가지로 의미로 사용되는데, 주로 클락 타이밍, 메모리 정렬등에 사용된다. 이 명령어를 만나면 메모리 액세스를 하면 안된다. 메모리 폴트 및 페이지 폴트를 만나게 될 것이다. 이 NOP은 각 아키텍처마다 고유의 값을 갖고 있다. x86에서는 `0x90` 값을 갖는다.

     

     

    - 컨트롤 레지스터

    : RDMSR

    " ECX에 들어있는 Model Specific Register(MSR)의 주소에서 값을 읽어 EDX:EAX에 쓴다. 이 때, MSR의 값은 64비트이다. 그래서 MSR의 상위 32비트는 EDX에 로드되고, 하위 32비트는 EAX에 로드된다. 이 명령어는 인자를 받지 않는다. 

    " EDX:EAX = MSR[ECX];

     

    : WRMSR

    " ECX에 들어있는 Model Specific Register의 주소에 EDX:EAX에 들어있는 값을 쓴다. RDMSR과 마찬가지로 EDX는 MSR의 상위 32비트로, EAX는 하위 32비트에 맵핑된다. 이 명령어도 인자를 받지 않는다.

    MSR[ECX] = EDX:EAX;

     

    - 나눗셈 & 나머지

    : 퍼포먼스 관련해서 나눗셈 연산과 나머지 연산이 상당히 느리다고 한다. 그래서 별도의 방법으로 처리를 한다고 한다. 2의 제곱승 형태라면, 오른쪽 쉬프트가 나머지 연산을 대체할 수 있고 나머지 연산은 AND으로 처리가 가능하다. 물론, 입력값이 2의 제곱승 형태여야 한다.

     

    - 루프문

    : x86에 루프 명령어가 별도로 존재하지만, 잘 사용되지 않는다. 퍼포먼스가 좋지 않다는 글이 많다.

     

     

     

     

    - 테크닉

    : 플래그 레지스터 읽기/쓰기

    " FLAGS 레지스터를 읽고 쓰기 위해서 스택 연산 명령어를 사용할 수 있다. PUSHF 명령어를 통해서 FLAGS 레지스터를 스택에 넣고, 꺼낼 때 일반 POP 명령어를 통해서 특정 레지스터에 FLAGS 레지스터의 값을 읽어올 수 있다. 그리고 PUSH 명령어를 통해서 특정 값을 스택에 넣고, POPF 명령어를 통해서 스택에 넣은 값을 FLAGS 레지스터에 쓸 수 있다. 예제는 다음과 같다.

    {
        ...
        pushfd
        pop eax
        ...
        ...
        push eax
        popfd
        ...
    }

     

    - 에러 사항

    : invalid 16-bit effective address

    " x86에는 레지스터의 종류중에 `인덱스/베이스 레지스터`라는 레지스터가 있다. 아래 코드를 NASM으로 빌드하면 `invalid 16-bit effective address` 에러가 발생한다. 그 이유는, 레지스터마다 기능이 정해져 있기 때문이다. 아래의 `CX` 레지스터가 오는 위치에 레지스터들을 `인덱스/베이스 레지스터`라고 한다. 이 레지스터들로는 `BX, BP, SI, DI`가 있다. `AX, CX, DX, SP`는 저 위치에 올 수 없다.

     mov al,[EcranVirtuel + cx]

    " 참고로 32비트에서는 이런 제약 사항이 존재하지 않는다.

    '임베디드 SW > 어셈블리어' 카테고리의 다른 글

    NASM  (2) 2023.08.12
    [어셈블리어] AArch64 어셈블리어  (0) 2023.08.03
Designed by Tistory.