ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 시스템프로그래밍 05 - 조금 더 상위 연산들
    쾌락없는 책임 (공부)/시스템프로그래밍 2021. 5. 30. 20:14
    반응형

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

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

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


    서론

    지금까지 배운 내용들을 본다면 단순한 값 대입같은 이야기들이라 상위 프로그래밍 언어로 되어 있는 것들이 어떻게 어셈블리어에서 동작하는지 알 수가 없습니다. 이번 포스팅부터는 루프문을 비롯한 조금 더 상위 개념들을 배운 뒤 정리한 내용들입니다.

     

    JUMP와 LOOP

     C언어에도 있는 내용으로 C의 점프와 루프와 하는 일이 같습니다. 대신 C에서는 다른 변수나 포인터 등을 통해서 이루어졌지만 어셈블리어에서는 다른 것들을 기준으로 해서 작동하게 됩니다.

     

    1. JMP

    JMP destination

     destination에는 레이블 이름이 올 수 있으며 해당 레이블로 점프를 하게 됩니다. C에서의 goto와 유사한 일을 하고 있으며 offset을 저장하는레지스터인 instruction pointer에 메모리 주소가 적재됩니다. 또한 이 구문을 만나면 무조건적으로 점프하게 됩니다.

     

    2. LOOP

    LOOP destination

      루프의 경우에도 위 점프처럼 destination으로 온 레이블로 점프를 하게 됩니다. 그런데 하나 다른 점이라면 점프의 경우 무조건적으로 점프를 하는 Unconditional 점프이지만 루프는 조건이 있는 Conditional 점프입니다.

     루프의 경우 ECX 레지스터에 있는 값을 사용해 점프를 하며 ECX에 있는 값을 1씩 줄여서 사용하게 되며 ECX가 0이 되면 루프를 더이상 돌지 않고 다음 명령어로 가게 됩니다.

     

     위 점프와 루프에서 주의할 점은 destination에 오는 주소가 -128~127 바이트 이내의 주소가 와야 합니다. 또한 ECX를 0으로 초기화를 하고 루프를 돌게 되면 루프는 1을 감소시킨 뒤 비교를 하기 때문에 FF...F값과 비교하게 되어서 오류가 나오게 됩니다. 추가적ㅇ로 코드를 짤 때 ECX 레지스터를 사용한다고 하면 (예: 다중 루프) 임시 변수에 저장해서 사용해야 합니다.

     

     

    STACK과 관련한 연산

     상위 언어에 있는 스택과 개념이 같으며 이 중 Runtime Stack을 사용하게 됩니다.(CPU가 제공) 여기서는 ESP 레지스터가 스택의 포인터로 사용되게 되며 ESP에 스택의 가장 최근 값의 offset를 저장하는 식으로 연산을 하게 됩니다.

     이런 스택은 레지스터를 위한 임시 저장소로 Call instruction을 사용할 때 돌아갈 주소를 저장하게 됩니다. 또한 함수를 만들었을 때 함수 값 등 전달한 값들을 스택을 사용해서 저장하기도 하며 서브루틴의 지역변수를 저장하기도 합니다.

     

    1. Push / Pop

     

    Push (ESP 감소) Pop (ESP 증가)
    PUSH reg/mem16 POP  reg/mem16
    PUSH reg/mem32 POP  reg/mem132
    PUSH  imm32  (32비트의 상수)  

     사용되는 operand들이 16비트라고 하면 2바이트, 32비트라고 하면 4바이트만큼 ESP가 증가, 감소하게 되면서 스택을 관리해 줍니다.

     

    2. PUSHFD / POPFD

    pushfd

    popfd

    따로 operand를 필요로 하지 않으며 32비트의 플레그 레지스터들이 위 연산에 영향을 받게 됩니다. pushfd를 하면 플래그 레지스터에 있는 값들이 스택에 저장되며 popfd를 하면 스택에 저장된 값들이 다시 플래그 레지스터에 들어오게 됩니다. 플래그 레지스터의 값은 직접 mov를 ㅌㅇ해서 제어할 수 없으니 이 방법을 통해서 플래그 레지스터의 값을 조절할 수 있습니다.

     

    3. PUSHA(D) / POPA(D)

     D가 붙어있다면 32비트와 관련한 연산이고 아니라고 하면 16비트의 연산입니다.

     PUSHA(D)의 경우 8개의 범용 레지스터 값들을 스택에 몽땅 자장하고 POPA(D)를 통해서 다시 불러오게 됩니다. 이 레지스터들이 저장되는 순서는 아래와 같습니다.

    32비트의 경우 16비트의 경우
    EAX / ECX / EDX / EBX / ESP / EBP / ESI / EDI AX / CX / DX / BX / SP / BP / SI / DI

      이 연산을 사용하는 이유는 프로시저의 서브루틴을 사용한 후 기존 레지스터에 있는 값들을 복원하기 위해서 사용하게 됩니다. 다만 서브루틴에서 리턴해야 하는 값들은 레지스터를 통해서 리턴되기 때문에 사용에 있어 주의를 가해야 합니다.

     

     

     

    프로시저

     어셈블리어에서 프로시저는 상위 언어에서의 함수와 비슷한 느낌을 가지고 있습니다. '프로시저 = 서브루틴' 이며 어떤 일을 수행하고 돌아오는것을 뜻하고 있습니다. 코드적으로 보면 이름을 가지고 있는 블록과 리턴으로 끝나는걸 프로시저라고 하고 있습니다.

     

    1. PROC directives

     어셈블리어를 보면 항상 나오는 것으로 main PROC 이런 식으로 사용하고 있습니다. 항상 main일 필요는 없으며 프로시저를 나타내는 지시어입니다.

     

    * 라벨의 경우 선언된 프로시저 안에서만 보이는 지역성을 가지고 있어 JMP, LOOP로 이동을 할 수 없습니다.

     - 다른 프로시저로 점프를 할 때는 destination 뒤에 ::을 붙여 이동할 수는 있지만 권장되는 방식이 아닙니다.

     

    2. CALL / RET Instruction

     CALL 명령어로 프로시저를 호출하게 하며 프로세서가 해당 메모리 주소에서 실행이 되게 합니다. RET는 상위 언어에서 보이는 return이며 프로시저 내부엣 원래 위치로 돌아오게 해줍니다.

     

    <명령어가 하는 일을 순서대로>

    1. call로 돌아올 위치를 스택에 저장

    2. call 프로시저 위치를 EIP에 복사

    3. RET으로 스택에 저장된 주소를 pop해서 EIP에 넣어줍니다.

     

    * 추가적으로 CALL의 경우 5바이트의 기계어로 번역되게 됩니다.

    * 프로시저의 return 값은 8개의 범용 레지스터에 넣어서 사용하게 됩니다.

      + 그런데 특정 레지스터가 프로시저 값을 전달하기로 되어있다면 위 방법으로 하기 불가합니다.

         이때 USES 연산자를 PROC와 함께 사용하게 되면 레지스터 값의 저장/복구를 어셈블러에게 부탁하게 됩니다. (ret전 pop 약속)

         다만 해당 프로시저가 계산 결과를 특정 레지스터에 넣어 전달할 경우 사용하면 안됩니다.

     

    외부 라이브러리 추가

     기계어 번역이 되는 프로시저의 집합 파일이 라이브러리이고 외부 라이브러리를 불러와 사용할 수 있습니다.

    Exlibrary proto

    call Exlibrary

    proto를 통해서 이러한 프로시저가 올것이라는걸 알려주고 call을 통해서 호출을 하게 됩니다. 이 프로시저는 blank로 남아 있다가 어셈블 후 링커가 프로시저를 찾아 copy를 해주게 됩니다. (이 과정에서 찾지 못한다면 오류가 나오게 됩니다)

     

    어셈블리어에서 대표적인 32비트 외부 라이브러리는 Irvine32로 간단한 IO 작업들이 들어있습니다.

    반응형

    댓글

Designed by Tistory.