R136A1

[WEEK1] 레지스터, 함수 호출규약(Calling Convention) 본문

SWING/[21-2] REVERSING

[WEEK1] 레지스터, 함수 호출규약(Calling Convention)

r136a1x27 2021. 9. 11. 16:21

레지스터 개요

CPU가 요청을 처리하는 데 필요한 데이터를 일시적으로 저장하는 기억장치

무리 없이 명령을 수행하기 위해 메모리보다 빨라야 함(실제로 수십 배~수백 배)

 

보조기억창치: 데이터를 영구적으로 저장

메모리: 임시적으로 저장

 

메모리로 연산의 "결과"를 보내고 보조기억장치에 데이터를 저장하는 "명령"을 처리하기 위해서는

이들에 대한 주소와 명령의 종류를 저장할 수 있는 기억 공간이 하나 더 필요

 

CPU에서 메모리로 데이터를 전송하기 위해서는 특정 주소를 가리키거나 값을 읽어오는 레지스터를 이용해야함.

CPU <-- 레지스터 --> 메모리

메모리에서 레지스터로 데이터를 복사해오는 데이터 전송 명령을 "적재(load)"라고 함

적재명령 = 연산자 이름 + 메모리에서 읽어온 값 저장할 레지스터 + 메모리 접근에 사용할 상수 + 레지스터

 

x86(32bit) x64(64bit)

레지스터 하나의 비트 수

레지스터는 주소를 나타내므로, CPU프로세서가 32비트인지 64비트인지에 따라 인식 가능한 메모리 용량이 달라진다

 

Timeline

- 16비트: 프로그램 간의 메모리 침범이 매우 빈번했음, 개발자들은 많은 공부 필요

    □X

- 인텔(Intel) 80386 processer

    32비트 프로세서에서는 아직도 80386의 구조(Architecture)를 그대로 사용하고 있음

    E□X

- 64비트

    R□X

데이터단위

BYTE      1 B   8비트 부호 없는 정수

WORD    2 B   16비트 부호 없는 정수

DWORD 4 B   32비트 부호 없는 정수

QWORD 8 B 64비트 부호 없는 정수

 

부호 없는 정수는 앞에 S만 붙이고 크기는 똑같다.

 

gyeongje.tistory.com/9

레지스터 종류

범용

EAX Accumulator 대부분의 입출력, 산술/논리 연산, (함수의) 처리 결과 리턴값 저장 곱셈과 나눗셈에서 자동으로 사용됨

EBX Base 간접 주소 연산, 주소 지정을 확장하기 위해 인덱스로서 사용 ESI나 EDI와 결합하여

ECX Counter 카운터, 반복문을 수행할 때 여기에 반복 횟수를 지정해놓고 작업 수행

EDX Data 큰 수의 산술/논리 연산 시 EAX의 보조 역할

인덱스

ESI Source Index 데이터 복사, 조작, 비교 대상의 주소

EDI Destination Index 데이터 복사할 곳의 주소

포인터

EBP Base Pointer 스택 영역의 기준이 되는 주소

         하나의 스택 프레임의 시작 지점 주소, 현재 사용되는 스택 프레임이 소멸되지 않는 동안 EBP의 값은 변하지 않음.

         소멸 시 이전에 사용되던 스택 프레임을 가리킴

ESP Stack Pointer 현재 사용중인 스택의 최상단 주소

         하나의 스택 프레임의 끝 지점 주소, PUSH POP 명령어에 따라서 4Byte씩 변함

명령포인터

EIP Instruction Pointer 다음에 실행해야 할 명령의 메모리 주소(거의 항상 활성화), 기본적으로 다음 줄 주소를 가리키며

                                                분기문을 사용할 경우 급변할 수 있다. (JMP, CALL, RETN)

 

세그먼트

CS Code Segment, 코드 영역 시작 주소, 함수와 제어문 같은 명령어들이 저장

DS Data Segment, 데이터 영역 시작 주소, 전역ㆍ정적 변수가 저장

SS Stack Segment, 스택 영역 시작 주소, 스택의 주소 저장(주소와 데이터를 일시적으로 저장할 목적으로 쓰이는)

ES┐

FSExtra Segment, 추가적인(보조) 데이터 세그먼트, 데이터 영역 시작 주소

GS

* 32bit 프로그램에서는 DS, Extra Segment가 같은 영역을 가리키고 있기 때문에 신경쓸 필요 없음

특히 FS, GS는 286 이후에 추가된 것으로 운영체제를 작성하는게 아니라면 없듯이 여겨도 됨

 

상태

EFLAGS CPU동작 제어, 연산 결과 1 또는 0으로 반영

FPU 수치연산

ST0 ~ 7 부동 소수점 처리

 

 

세그먼트 레지스터

현재 메모리 주소를 표현하기 위한 세그먼트 디스크립터 테이블(SDT: Segment Descriptor Table)의 위치를 나타냄

각 duid역들의 기본 위치를 가리킴

 

운영체제는 메모리를 효율적으로 이용하기 위해 "가상메모리"라는 기술을 통해

실제 물리적 메모리보다 많은 메모리를 이용할 수 있도록 함

 

프로세서가 메모리에 접근할 때, 세그먼트 레지스터를 이용하여 가상 메모리를 실제 물리적 메모리로 변경함

 

모드에 따라 세그먼트 레지스터 사용 방식이 달라짐

리얼 모드

메모리에 직접 접근하는 방식

보호 모드

메모리에 가상 접근하는 방식

 

대부분의 OS들은 각 프로세스들이 사용할 수 있는 메모리를 가상으로 제공하는 보호 모드로 동작

메모리 관리는 OS가 담당하는데, 이 때 사용하여 참조되는 것이 세그먼트 디스크립터 테이블

EFLAGS

Extend-Flags 여러가지 상태 저장에 사용됨

(시스템 제어용 플래그, 어셈블리의 처리 조건 용도...)

하나의 레지스터에 비트 단위로 용도가 배정되어있음

 

지금은 설명이 무슨 말인지...Flag야 말로 이론보다 분석하면서 배우는 분야인듯

연산 결과

CF Carry Flag                    덧셈과 뺄셈에서 빌림수 발생 시 1로 설정

PF Parity Flag                   연산 결과가 짝수면 1, 홀수면 0으로 설정

AF Auxiliary-carry Flag 16비트 연산 시 빌림수 발생 시 1로 설정

ZF Zero Flag                      연산 결과가 0이면 1로 설정

SF Sign Flag                      연산 결과의 최상위 비트가 1인 경우 1로 설정

OF Overflow Flag            연산 결과가 용량을 초과하였을 경우 1로 설정

 

시스템 제어

TF Trap Flag                           프로그램 추적(Trace)시 1로 설정, 명령을 한 행씩 실행하도록 함

IF Interrupt enable Flag    외부 인터럽트 요구를 받아들일 때 1로 설정

AC Alignment Check CR0  레지스터의 AM 비트와 함께 1로 설정하면 메모리 참조 시 정렬 체크를 활성화

IOPL I/O Privilege Level     현재 특권 수준이 IOPL보다 높은 경우 I/O 주소 접근

NT Nested Task                     연결 작업 제어, 1로 설정 시 현재 작업이 기존 실행 작업과 연결됨을 의미

RF Resume Flag                    디버깅 시 프로세서에 일시 중지 예외 발생 제어

ID ID Flag                                 디버깅 시 프로세서에 일시 중지 예외 발생 제어

VM Virtual-8086 Mode                 디버깅 시 프로세서에 일시 중지 예외 발생 제어

VIF Virtual Interrupt Flag           디버깅 시 프로세서에 일시 중지 예외 발생 제어

VIP Virtual Interrupt Pending   디버깅 시 프로세서에 일시 중지 예외 발생 제어


함수 호출규약(Calling Convention)

: 파라미터를 전달하는 방법에 대한 약속

호출자(caller)와 피호출자(callee)간의 함수의 인자를 전달하는 방식에 대한 규약을 정의한 것

 

용어

인자(argument): 함수의 인자

호출자(caller): 호출한 함수

피호출자(callee): 호출된 함수

 

호출규약의 요소

1) 파라미터, 반환값, 반환주소가 배치되는 위치(레지스터/(콜)스택/메모리...)

2) 형식 매개변수에 대한 실제 인수가 전달되는 순서

3) 피호출자의 반환값이 어떻게 호출자로 전달되는지 (레지스터/스택/힙...)

4) 함수 호출을 위한 세팅 및 정리 업무를 피호출자와 호출자 사이에서 구분하는지

5) 인자를 설명하는 메타데이터의 전달 여부 및 방법6) 루틴 종료 시 프레임 포인터를 복구할 때의 값을 어디에 저장해둘건지(스택프레임/레지스터...)

7) 루틴의 비지역 데이터 접근에 대한 정적 스코프 링크가 배치되는 위치 (?) (스택프레임/레지스터...)

8) 지역 변수가 어떻게 할당되는지

 

 

변형

아키텍처마다, 프로그래밍 언어에 따라, 컴파일러의 구현에 따라 다르게 정의되거나 구현될 수 있음

x86 (32비트)

(1) __cdecl (C declaration)

: C와 가변인자를 사용하는 C++함수의 기본 호출 규약

함수 이름 앞에 _ 가 붙음

인자는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

호출자(callee)가 스택포인터를 정리(복원) (C: 내부적으로 / assembly: 직접)

 

(2) __stdcall (Standard Call)

: Win32 API의 기본 호출 규약

함수 이름 앞에 _ 가 붙음

함수 이름 끝에 @가 붙음

인자는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

피호출자(caller)가 스택포인터를 정리(복원)

 

[장점]

① 함수의 독립성이 좋다

② __cdecl 방식보다 코드 양이 적다

  (여러곳에서 호출되더라도 스택 정리 코드는 함수 내 1번만 존재하기 때문 - caller가 함)

 

(3) __fastcall

: Intel CPU 전용

함수 이름 앞에 @가 붙음

첫번째 인자는 ECX, 두번째 인자는 EDX 레지스터를 사용한다

세번째 인자부터는 스택을 사용한다

오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

(즉, 인자 ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ → ⓒ 순서대로 스택에 push / ⓑ EDX ⓐ ECX)

피호출자(caller)가 스택포인터를 정리(복원)

 

[장점]

① __stdcall과 거의 동일하지만 2개 이하의 인자를 사용할 때 레지스터를 이용하여 속도가 더 빠르다

 

(4) __thiscall

: 가변인자를 사용하지 않는 C++함수의 기본 호출 규약

인자는 스택을 사용해서 오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

피호출자(caller)가 스택포인터를 정리(복원)

 

직접적으로 호출 규약을 사용할 수 없다

ECX에 클래스의 this포인터를 전달한다.

멤버함수는 __thiscall을 사용하지만, 직접 지정해서 다른 호출규약을 사용할 수 있다.

(*다른 호출규약 사용 시 첫번째 인자로 this 포인터가 전달됨)

 

 

[스택정리방식]

__cdecl을 제외한 나머지 함수 호출 규약이 피호출자(caller)가 스택을 정리하는 이유?

> __cdecl 방식은 ret 시 [POP EIP / JMP EIP]와 같이 복귀주소를 꺼내고 리턴한다.

이후 호출자(callee) 쪽에서 ADD 명령어를 이용해서 스택을 복원한다

 

> 나머지 함수 호출 규약은 RET 8 시 POP EIP와 같이 우선 복귀주소를 꺼낸다

POP를 2번 하고 JMP EIP로 복귀한다

 

x64 (64비트)

64비트는 하나의 호출 규약만 사용한다

__fastcall을 업그레이드한 호출 규약 (단, 같은 호출 규약이지만 파일 포맷에 따라 세부 규약이 존재한다)

피호출자(caller)가 스택포인터를 정리(복원)

 

(1) ELF (OS: Linux)

1. 인자가 정수일 때

1~6번째 인자는 각각 RDI, RSI, RDX, RCX, R8, R9 총 6개의 레지스터를 사용한다

7번째 인자부터는 스택을 사용한다

오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

(즉, 인자 ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ  순서대로 스택에 push / ⓕ R9 ⓔ R8 ⓓ RCX ...)

 

 

2. 인자가 실수일 때

1~8번째 인자는 각각 XMM0 ~ XMM7 총 8개의 레지스터를 사용한다

9번째 인자부터는 스택을 사용한다

오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

(즉, 인자 ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ  순서대로 스택에 push / ⓗ XMM7 ⓖ XMM6 ⓕ XMM5 ...)

 

(1)PE (OS: Windows)

※ 파라미터가 4개 미만인 상태로 함수를 호출해도 4개의 레지스터를 위한 영역을 할당해야 함

1. 인자가 정수일 때

1~4번째 인자는 각각 RDI, RSI, RDX, RCX, R8, R9 총 4개의 레지스터를 사용한다

5번째 인자부터는 스택을 사용한다

오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

(즉, 인자 ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ  순서대로 스택에 push /  R9  R8  RCX ...)

 

2. 인자가 실수일 때

1~4번째 인자는 각각 XMM0 ~ XMM3 총 4개의 레지스터를 사용한다

5번째 인자부터는 스택을 사용한다

오른쪽에서 왼쪽 순서로 전달(저장) (사용할 순서의 역순)

(즉, 인자 ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ →  순서대로 스택에 push /  XMM3  XMM2  XMM1 ...)

 


PUSH로 스택에 저장 / MOV [레지스터명], [값] 으로 저장

ex) printf("%d, %d, %d, %d, %d, %d", a, b, c, d, e, f);
push f

mov r9, e

mov r8, d

mov rcx, c

mov rdx, b

mov rsi, c

lea rdi, "%d, %d, %d, %d, %d, %d \0"

call printf의 주소(push rip / jmp printf의 주소)

 

 

[참고/상세]

https://ccurity.tistory.com/15  

https://minusi.tistory.com/entry/함수-호출-규약Calling-Convention

Comments