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을 하지 않아야 한다.
'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 |