본문 바로가기

CS/System programming

Procedures

728x90

Procedures

Stack Operations

  • 이 과목에서는 런타임 스택에 집중한다.
    • 프로시저 호출과 반환 메커니즘의 중요한 부분이다.
    • 런타임 스택은 서브루틴 호출을 처리하기 위한 시스템 레벨에서 작동한다.
  • 서브루틴: 일련의 작업을 수행하는 코드 블록, 인자 값을 받아들일 수 있고 결과 값을 반환할 수 있다.

1. Runtime Stack (32-Bit Mode)

  • CPU가 직접 관리하는 메모리 배열

  • ESP 레지스터를 사용하여 관리된다. 32비트 모드에서 스택의 어느 위치에 대한 32비트 오프셋을 가지고 있다.

  • ESP는 항상 스택의 맨 위에 추가되거나 푸쉬된 가장 최근 값에 대한 오프셋을 가리킨다.

  • Push Operation

    • 스택 포인터가 4만큼 감소한다.
    • 스택 포인터가 가리키는 위치에 값을 복사한다.
    • 런타임 스택은 높은 주소에서 낮은 주소 방향으로 성장
  • Pop Operation

    • 스택에서 값을 제거한다.
    • 스택 포인터를 증가시킨다.
  • Stack Applications (스택의 응용)

    • 레지스터가 여러번 사용되는 경우, 일시적으로 저장하기 위한 영역 제공
    • 수정된 레지스터 원래 값으로 복귀 가능
    • CALL 명령어가 실핼될 때, CPU는 현재 서브루틴의 반환 주소를 스택에 저장
    • 서브루틴을 호출할 때, 인자(argument)를 스택에 push하여 전달
    • 스택은 서브루틴 내에서 지역 변수를 일시적으로 저장하기 위한 영역 제공

2. PUSH and POP Instructions

  • PUSH Instruction

    • 16 비트의 연산자의 경우 ESP를 2 감소시킨다.

    • 32 비트는 4 감소시킨다.

    • 소스 피연산자를 스택에 복사한다.

    • 포맷

      PUSH reg/mem16
      PUSH reg/mem32
      PUSH imm32
  • POP Instruction

    • ESP가 가리키고 있는 스택의 내용을 16비트 or 32비트 destination 피연산자에 복사한다.
    • 그런 다음 ESP를 증가시킨다.
  • PUSHFD and POPFD Instructions

    • PUSHFD(push flag double-words): 32비트 EFLAGS 레지스터를 스택에 푸시한다.

    • POPFD: 스택에서 값을 꺼내어 EFLAGS에 저장한다.

    • MOV 명령은 플래그를 변수에 복사할 수 없으므로 PUSHFD가 가장 좋은 방법이다.

      ; 예시
      pushfd      ; 플래그 스택에 저장
      ;
      ;
      ;
      popfd       ; 플래그 다시 가져옴
    • 오류가 덜 발생하게 짜는 법

      .data
      saveFlags DWORD ?
      .code
      pushfd              ; 플래그를 스택에 push
      pop saveFlags       ; 스택에서 변수로 복사
      
      push saveFlags      ; 변수에 있는 걸 스택에 push
      popfd               ; 스택에 있는 것을 다시 레지스터에 복사
  • PUSHAD, PUSHA, POPAD, POPA

    • PUSHAD는 다음 순서로 32비트 범용 사용 레지스터를 모두 스택에 푸쉬한다.

      • EAX, ECX, EDX, EBX, ESP, EBP, ESI 및 EDI
    • POPAD 명령은 역순으로 동일한 레지스터를 스택에서 팝한다.

    • 16비트에서는 AX, CX, DX, BX, SP, BP, SI, DI

      • 16비트 모드에서 프로그래밍할 때에만 PUSHA와 POPA를 사용해야 한다.
    • 여러 개의 32비트 레지스터를 수정하는 프로시저를 작성하면, 프로시저의 시작 부분에 PUSHAD, 끝 부분에 POPAD를 사용하여 레지스터를 저장하고 복원한다.

    • 하나 이상의 레지스터에서 결과를 반환하는 프로시저는 PUSHA와 PUSHAD를 사용하면 안된다.

    • ReadValue 프로시저가 EAX에 정수를 반환한다고 가정하면

      ReadValue PROC
      pushad
      .
      .
      mov eax, return_value
      .
      .
      popad
      ret
    • POPAD 호출은 EAX에서 반환 값을 덮어쓰게 된다.

    • 문자열 뒤집기 예제

      .data
      aName BYTE "Abraham Lincoln", 0
      nameSize = ($ - aName) - 1
      
      .code
      mov ecx, nameSize
      mov esi, 0
      
      L1: movzx eax, aName[esi]
        push eax
        inc esi
        loop L1
      
        mov ecx, nameSize
        mov esi, 0
      
      L2: pop eax
        mov aName[esi], al
        inc esi
        loop L2

Defining and Using Procedures

  • 어셈블리어에서 서브루틴을 뜻하는 프로시저라는 말을 쓴다.

1. PROC Directive

  • 프로시저 정의

    • 반환문으로 끝나는 일련의 문장으로 구성된 이름이 지정된 블록
    • PROC 및 ENDP 지시어를 사용하여 선언된다.
    • 유효한 식별자로 이름이 지정되어야 한다.
    • 프로그램의 시작 프로시저 이외의 프로시저를 생성할 때는 RET 명령으로 끝내야 한다.
    • RET은 프로시저가 호출된 위치로 반환하도록 한다.
  • Label in Procedures

    • 레이블은 선언된 프로시저 내에서만 볼 수 있다.
    • 이 규칙은 종종 점프 및 루프 명령에 영향을 미친다.
    • 예를 들어, JMP 명령어와 동일한 프로시저 내에 위치한 Destination이라는 레이블이 있어야 한다. (jmp Destination)
    • 더블 클론(::)을 통해 전역 레이블을 선언할 수 있다.
    • 하지만 프로그램 디자인 관점에서 현재 프로시저를 벗어나서 점프 또는 루프를 하는 것은 좋지 않다.
      • 프로시저는 자동으로 스택을 반환하고 조정한다. 프로시저에서 직접 전환하는 경우 스택이 손상될 수 있다.
  • 예시: 세 정수의 합

    • 세 개의 32비트 정수의 합

    • 해당 정수가 프로시저에 호출되기 전에 EAX, EBX, ECX에 할당되었다고 가정

    • 프로시저는 EAX에 합계를 반환

      SumOf PROC
        add eax, ebx
        add eax, ecx
        ret
      sumOf ENDP
  • 프로시저 문서화

    • 프로그램에 명확하고 읽기 쉬운 문서를 추가하는 것은 좋은 습관이다.

    • 각 프로시저에 포함할 수 있는 몇 가지 정보

      • 프로시저가 수행하는 작업에 대한 설명
      • 입력 매개변수 및 사용 방법 목록, 'Receives'로 레이블을 지정
      • 프로시저가 반환하는 모든 값에 대한 설명, 'Returns'로 레이블 지정
      • 호출하기 전에 충족되어야 하는 조건 목록, 'Requires'
      ;-------------------------------------
      ; sumof
      ; Calculates and returns the sum of three 32-bit integers
      ; Receives: EAX, EBX, ECX, the three integers. My be
      ;           signed or unsigned.
      ; Returns: EAX = sum
      ;-------------------------------------
      SumOf PROC
        add eax, ebx
        add eax, ecx
        ret
      sumOf ENDP

2. CALL and RET Instructions

  • CALL 명령어: 프로시저를 호출하여 프로세서가 새로운 메모리 위치에서 실행을 시작하도록 지시

  • RET 명령어: 프로시저는 RET 명령어를 사용하여 프로시저를 호출한 지점으로 프로세서를 되돌린다.

  • Mechanical point of view

    • CALL 명령은 스택에 반환 주소 푸시
    • CALL 명령은 호출된 프로시저의 주소를 instruction pointer에 복사
    • 반환할 때, RET 명령어는 반환 주소를 스택에서 instruction pointer로 팝한다.
    • 32비트 모드에서는 CPU는 EIP가 가리키는 메모리 위치에서 명령어 실행
  • Call and Return EX

    • 메인 함수에서 오프셋 00000020에 CALL문이 있다고 가정
    • 이 명령문은 5바이트가 필요하므로 다음 문은 offset 00000025에 위치한다.
    • MySub의 첫 실행 가능한 명령문이 offset 00000040에 위치한다고 가정
    • CALL 명령문이 실행되면, CALL 다음의 주소(00000025)가 스택에 푸시되고, MySub의 주소가 EIP에 로드
    • MySub의 모든 명령문이 실행되고 RET 명령문까지 도달
    • RET 명령문이 실행되면 ESP가 가리키는 스택 값이 EIP로 팝되고, ESP가 이전의 스택 값으로 향하도록 증가
  • Nested Procedure Calls

    • 호출된 프로시저가 반환하기 전에 다른 프로시저를 호출하는 경우
    • 예를 들어, main이 Sub1 프로시저를 호출하면 Sub1이 실행하는 동안 Sub2 프로시저를 호출할 수 있다.
    • Sub2가 실행 중일 때 Sub3를 호출하면 Sub3의 RET 명령문이 실행되면서 ESP에 있는 값을 반환 주소로 가져와 실행이 Sub2로 돌아간다.
    • Sub2가 반환되면 다시 Sub1로 돌아가고 Sub1이 반환되면 main으로 돌아간다.
  • Passing Register Arguments to Procedures

    • 정수 배열의 합을 계산하는 등 일반적인 작업을 수행하는 프로시저를 작성한다면, 프로시저 내에 특정 변수 이름에 대한 참조를 포함하는 것은 좋지 않다. 그렇게 하면 프로시저가 하나의 배열만 이용할 수 있기 때문이다.

    • 더 좋은 방법은 배열의 오프셋을 프로시저에 전달하고, 배열 요소의 개수를 지정하는 정수를 전달하는 것이다. 이것을 input parameter라고 한다.

    • 어셈블리어에서는 범용 사용 레지스터 내부에 인수를 전달하는 것이 일반적이다.

    • Sumof: EAX, EBX, ECX 레지스터에 있는 정수를 더하는 프로시저

      .data 
      theSum DWORD ?
      .code
      mov eax, 10000h
      mov ebx, 20000h
      mov ecx, 30000h
      call Sumof
      mov theSum, eax
  • 정수 배열의 합계를 구하는 예시

    • ArraySum은 호출 프로그램으로부터 두 개의 매개변수를 받는다. 32비트 정수형 배열의 포인터와 배열의 값 개수

    • 이 프로시저는 배열의 합을 계산하여 EAX레지스터에 저장하고 반환한다.

    • 이 프로시저 안에서는 특정한 배열 이름이나 크기에 대한 내용이 없다

      ArraySum PROC
        push esi                ; save ESI, ECX
        push ecx
        mov eax, 0              ; set the sum to zero
      
      L1: add eax, [esi]          ; 정수를 sum에 더한다.
        add esi, TYPE DWORD     ; 다음 정수를 가리킨다.
        loop L1
      
        pop ecx                 ; restore ECX, ESI
        pop esi
        ret                     ; sum is in EAX
      ArraySum ENDP

6. Saving and Restoring Register

  • 위의 예제에서 ECX, 와 ESI는 프로시저의 시작 부분에서 스택에 푸시되었고, 끝에서 팝되었다.

  • 프로시저에서 수정된 레지스터를 항상 저장하고 복원해서 호출 프로그램이 자신의 레지스터 값이 덮어써지지 않도록 보장해야 한다.

  • 위 규칙의 예외는 주로 EAX와 같이 반환 값으로 사용되는 레지스터이다. 이러한 레지스터는 푸시하고 팝하지 않아야 한다.

  • USES Operator

    • PROC 지시문과 결합하여, 프로시저 내에서 수정된 모든 레지스터 이름을 나열할 수 있다.

    • USES는 어셈블러에게 두 가지 작업을 수행하도록 지시한다.

      • 첫째, 프로시저 시작 시 레지스터 값을 스택에 저장하도록 하는 PUSH 명령어 생성한다.
      • 둘째, 프로시저 끝나면 레지스터 값 복원하는 POP 명령어를 생성
    • USES 다음 쉼표 사용하지 않음

      ArraySum PROC USES esi ecx
        mov eax, 0              ; set the sum to zero
      
      L1: add eax, [esi]          ; 정수를 sum에 더한다.
        add esi, TYPE DWORD     ; 다음 정수를 가리킨다.
        loop L1
      
        ret                     ; sum is in EAX
      ArraySum ENDP
  • Exception

    • 어떤 프로시저가 레지스터에 값을 반환하는 경우, 그 반환 레지스터는 PUSH와 POP을 하지 않아야 한다.
728x90

'CS > System programming' 카테고리의 다른 글

Data Transfers Addressing and Arithmetic  (0) 2023.04.08
Assembly Language Fundamentals  (0) 2023.03.20
X86 Processor Architecture  (0) 2023.03.02
Basic concept  (0) 2023.02.23