728x90

힙, 데이터 영역, 그리고 코드영역의 공유에 대한 검증

쓰레드는 메모리를 공유한다. 특히 전역변수가 할당되는 데이터 영역과, 메모리가 동적으로 할당되는 힙영역을 공유한다.

 

 

메모리 동시접근 문제

전역변수에 둘 이상의 쓰레드가 동시 접근을 할 수 있다.

실제로는 쓰레드는 돌아가면서 실행된다. 전역변수에 접근하여 연산을 하기 위해서는 메모리에 저장된 데이터를 레지스터로 이동시켜야한다. 이후 ALU에 의해 실질적인 덧셈 연산이 진행되고, 그 결과가 다시 메모리에 저장되는 구조로 진행이딘다. 여러 쓰레드에서 하나의 전역변수를 연산에 사용할 때는 실행중인 쓰레드의 변경에 의해서 컨텍스트 스위칭이 빈번하게 발생한다. 때문에 둘 이상의 쓰레드가 같은 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높다.

 

프로세스로 부터의 쓰레드 분리

프로세스는 쓰레드를 담는 상자역할을 한다. 그리고 핸들 테이블 또한 프로세스 소유이다. 즉 하나의 프로세스에 하나의 핸들 테이블이 존재한다. 또한 같은 프로세스 내에서 생성된 모든 쓰레드들은 스택 이외의 모든것을 공유하기 때문에 핸들 테이블도 공유한다.

 

ANSI 표준 C 라이브러리와 쓰레드

멀티 쓰레드 기반 라이브러리를 설정하고 쓰레드를 생성할 때, CreateTread함수가 아니라 _beginthreadex 함수를 사용해야 한다. _beginthreadex 함수도 내부적으로는 쓰레드 생성을 위해서 CreateThread함수를 호출 한다. 다만 쓰레드를 생성하기에 앞서서 쓰레드를 위해, 독립적인 메모리 블록을 할당해준다는 차이점이 있다. 각각의 메모리 블록을 할당 함으로써 멀티쓰레드의 안정성이 확보된다.

 

_beginthreadex 함수

uintptr_t _beginthreadex(

    void * security,

    unsigned stack_size,

    unsigned (*start_address)(void *),

    void * arglist,

    unsigned initflag,

    unsigned *thrdaddr

);

함수의 전달인자의 순서와 의미는 CreateThread함수와 동일하다. 다만 매개변수 자료형과 반환형에 차이가 있어, 약간의 형 변환이 요구된다.

_beginthreadex 함수를 사용하기 위해서는 헤더파일 process.h를 추가로 포함시켜야 한다.

 

_beginthradex 함수를 종료할 때 가장 좋은 방법은 return문을 이용하는 것이다.

ExitThread 함수를 활용해야할 상황이라면 _endthreadex(unsigned retval);를 사용하는 것이 좋다. _endthreadex이 함수는 내부적으로 쓰레드에 할당된 메모리를 해제하고 ExitThread함수를 호출한다. 쓰레드 함수에서 return 문을 이용하더라도 _endthreadex함수가 자동으로 호출된다. 

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

쓰레드 동기화  (0) 2020.07.28
쓰레드 상태 컨트롤  (0) 2020.07.26
Windows에서의 쓰레드 생성과 소멸  (0) 2020.07.23
쓰레드 구현 모델에 따른 구분  (0) 2020.07.22
쓰레드의 이해  (0) 2020.07.20
728x90

쓰레드 생성

windows에서 쓰레드 생성 함수는 CreateThread이다.

함수 선언

HANDLE CreateThread(

    LPSECURITY_ATTRIBUTES lpThreadAttributes,

   SIZE_T dwStackSize,

   LPTHREAD_START_ROUTINE lpStartAddress,

   LPVOID lpParameter,

   DWORD dwCreationFlags,

   LPDWORD lpThreadId

);

1.lpThreadAttributes : 핸들의 상속 여부 결정, NULL이 전달 되면 상속X

2.dwStackSize : 쓰레드의 스택 크기를 지정하기 위한 매개변수다.

                     0이 전달되면 디폴트 크기인 1M바이트가 적용된다.

3.lpStartAddress : 쓰레드의 main역할을 하는 함수를 지정하는 전달인자이다. 인자 타입이 LPTHREAD_START_ROUTINE                          인데 이는 다음과 같이 정의되어 있는 함수 포인터 이다.

typedef DWORD(WINAPI * PTHREAD_START_ROUTINE)

                     (LPVOID lpThreadParameter);

typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

변환 타입이 DWORD이고 매개변수 타입은 LPVOID(void *)인 형태로 정의되어야 한다.

4.lpParameter : 쓰레드 함수에 전달할 인자를 지정하는 용도로 사용한다. main함수의 argv와 유사하다.

5.dwCreationFlags : 쓰레드의 생성 및 실행을 조절하기 위해 사용되는 전달인자다.

                          인자로 CREATE_SUSPENDED가 전달되면, 쓰레드는 생성과 동시에 Blocked상태에 놓이게 된다.                                 ResumeThread 함수가 호출되면 실행을 시작한다.

                           STACK_SIZE_PARAM_IS_A_RESERVATION을 전달할 경우 dwStackSize를 통해 전달되는 값의                                     크기는 reserve메모리 크기를 의미하게 되고, 그렇지 않을 경우 commit 메모리 크기를 의미하게                               된다.

6.lpThreadId : 쓰레드 ID를 전달받기 위한 변수의 주소값을 전달한다.

7.마지막으로 함수호출이 성공하면 생성된 쓰레드의 핸들이 반환된다.

 

생성할 수 있는 쓰레드의 최대 개수는 메모리가 허용하는 만큼이다. 쓰레드가 생성될 때마다 독립된 스택을 할당해 줘야만 한다. 그렇기 때문에 스택을 할당할 수 있을 때까지 쓰레드의 생성을 허용한다.

main 쓰레드의 return문은 프로세스의 종료를 의미하고 쓰레드 함수의 return문은 쓰레드 종료를 의미한다.

 

멀티 쓰레드 기반 프로그래밍에서 쓰레드의 흐름을 예측하는 것은 불가능 하다. 시스템의 당시 상황에 따라 다르게 동작하기 때문에 예측 자체가 의미가 없다.

 

스택은 쓰레드별로 독립적이기 때문에 쓰레드에 할당해주는 스택이 커질수록 쓰레드의 개수는 줄어든다.

 

Sleep함수

VOID Sleep(

          DWORD dwMilliseconds

);

1.dwMilliseconds : 쓰레드의 실행을 멈추기 위한 시간 정보를 Milliseond 단위로 지정한다. 0을 인자로 전달할 경우 자신에게 할당된 타임 슬라이스(Time Slice)를 포기하고 우선순위가 같은 다른 쓰레드에게 실행의 기회를 양보한다.

 

쓰레드 소멸

쓰레드 함수 내에서 return문을 통해 종료 및 소멸시키는 방법이 가장 이상적이다. 일반적으로 쓰레드 종료 방법에 있어서 return 이외의 방법은 거의 생각하지 않는다. 그러나 return외의 종료방법이 적절한 경우도 존재한다.

 

case 1 : 쓰레드 종료시 return 을 이용하면 좋은 경우

일반적인 경우에 return을 종료로 사용하는 것이 좋다. 특정한 연산의 결과값을 반환 할때도 return을 사용하는 것이 좋다. 쓰레드 함수가 반환한 결과값은 프로세스 종료코드 처럼 커널 오브젝트에 결과가 저장된다.

저장된 종료코드는 main쓰레드에서 GetExitCodeThread함수를 통해 가져올 수 있다.

BOOL GetExitCodeThread(

      HANDLE hThread,

      LPDWORD lpExitCode

);

1.hThread : 종료코드를 얻기 위한 쓰레드의 핸들을 인자로 전달한다.

2.lpExitCode : 얻게되는 종료코드를 저장할 메모리의 주소값을 전달한다.

 

case 2 : 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우(특정 위치에서 쓰레드의 실행을 종료시키고자 하는 경우)

ExitThread 함수가 존재한다. 이 함수는 현재 실행중인 쓰레드를 종료하고자 할 때 호출하는 함수로 return 방식의 쓰레드 종료만큼 선호된다.

VOID ExitThread(

      DWORD dwExitCode

);

dwExitCode : 커널 오브젝트에 등록되는 쓰레드 종료코드(Exit Code)를 지정

                  위 함수의 장점은 언제 어디서나 쓰레드를 종료시킬 수 있다는 점이다. 그렇다면 return 종료와 무엇이                        다른가.

 

예를 들어 C함수가 호출된 상태에서 쓰레드를 종료해야 한다면, ExitThread함수를 호출하는 것이 더 간결하다. return에 의한 종료를 원한다면 threadfunction까지 return을 해야만 종료가 가능해진다.

 

case 3 : 쓰레드 종료 시 TerminateThread 함수 호출이 유용한 경우(외부에서 쓰레드를 종료하고자 하는 경우)

main함수에서 쓰레드를 생성할 경우 해당 쓰레드의 핸들을 얻게된다. 이 핸들을 이용해서 쓰레드를 강제 종료시킬 수 있다. 순전히 외부에 의한 강제 종료이다.

BOOL TerminateThread(

     HANDLE hThread,

     DWORD dwExitCode

);

1.hThread : 강제 종료할 쓰레드 핸들.

2.dwExitCode : 종료할 쓰레드의 종료코드를 인자로 전달한다. 이 종료코드는 해당 쓰레드의 커널 오브젝트에 등록된다.

 

위 함수의 문제점은 강제 종료라는 점이다. 종료의 대상이 되는 쓰레드는 종료가 되는 시점까지도 자신이 종료된다는 것을 인식하지 못한다. 따라서 종료에 필요한 여러가지 일들(메모리 해제, 리소스 해제 등등)을 처리하지 못하고 종료된다.

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

쓰레드 상태 컨트롤  (0) 2020.07.26
쓰레드의 성격과 특성  (0) 2020.07.25
쓰레드 구현 모델에 따른 구분  (0) 2020.07.22
쓰레드의 이해  (0) 2020.07.20
함수 호출 규약  (0) 2020.07.18
728x90

커널레벨(kernel Level)쓰레드와 유저 레벨(User Level)쓰레드

쓰레드를 생성해주는 대상은 커널일 수 있다.

이러한 경우 운영체제가 제공하는 시스템 함수 호출을 통해 쓰레드 생성을 요구해야한다. 이후 운영체제는 해당 쓰레드를 생성 및 관리하면서 새로운 흐름을 형성하도록 도와준다.

 

프로그래머 요청에 따라 쓰레드 생성 및 스케줄링하는 주체가 커널인 경우, 이를 가리켜 커널 레벨(kernel Level)쓰레드라 한다.

 

유저  영역(User 영역)은 사용자에 의해서 할당되는 메모리 공간이다. 코드 영역, 데이터 영역, 스택 및 힙 영역을 가리켜 유저 영역(User 영역)이라 한다.

 

커널 영역은 하낭에 프로세스에 할당된 총 메모리 공간 중에서 유저영역을 제외한 나머지 영역을 커널 영역이라 한다. 운영체제라는 하나의 소프트웨어를 실행시키기 위해서 필요한 메모리 공간을 커널영역(kernel 영역)이라 한다.

 

쓰레드에게 일을 시키기 위한 코드는 프로그래머가 개발하므로 쓰레드 A, B, C의 실행코드는 유저영역에 존재할 것이다. 스케줄러와 쓰레드(스케줄링 하는데 필요한 쓰레드 정보)는 커널영역에 존재한다. 이것이 커널레벨 쓰레드의 유형이다.

 

유저레벨(User Level)쓰레드

커널에 의존적이지 않은 형태로 쓰레드의 기능을 제공하는 라이버리를 활용할 수 있는데, 이러한 방식으로 제공되는 쓰레드가 유저레벨 쓰레드이다. 커널에서 제공하는 기능이 아니므로 실행시 유저영역에서 실행된다.

 

위 그림은 쓰레드를 지원하지 않는 운영체제에서의 유저레벨 쓰레드 모델을 적용한 그림이다. 

운영체제가 쓰레드를 지원하지 않기 때문에 스케줄러가 스케줄링하는 대상은 프로세스이다. 쓰레드를 스케줄링하는 스케줄러는 유저영역에서 실행된다. 유저레벨 쓰레드 모델을 적용할 경우, 운영체제는 쓰레드의 존재를 알지도 확인하지도 못한다.

 

커널모드(kernel Mode)와 유저 모드(User Mode)

Windows 운영체제는 동작할 때 커널모드와 유저모드 중 한가지 모드로 동작한다.

 

메모리는 활용대상에 따라서 유저영역과 커널영역으로 나뉜다. 유저영역은 사용자가 구현한 프로그램 동작시 사용하게 되는 메모리영역이다.

커널영역은 운영체제 동작시 사용하는 메모리 영역이다. 커널이 쓰레드를 지원할 경우 쓰레드 관리가 커널영역에서 이뤄지기 때문에 커널레벨 쓰레드 모델이라 하고, 커널이 지원하지 않을 경우에 라이브러리를 통해서 제공받아야하는데 이러한 경우 유저영역에서 쓰레드의 관리가 이뤄지기 때문에 유저레벨 쓰레드 모델이라한다.

 

유저영역에서의 메모리 참조 오류는 실행중인 프로그램에만 영향을 미치게 되지만, 커널 영역은 커널 코드가 실행되는 영역이므로 시스템 전체에 영향을 줄 수 있다.

 

일반적인 프로그램은 유저모드에서 동작한다. 그러다가 커널이 실행되어야 하는 경우에는 커널모드로의 전환이 일어난다. 즉 커널영역에서 실행해야할 경우 커널모드로 전환된다. 예를 들어 프로세스가 정해진 타임슬라이스가 지나고 스케줄러가 동작하려 할때 커널모드로의 전환이 일어난다. 스케줄러는 커널의 일부에 해당하기 때문이다.

 

커널모드와 유저모드의 차이점

 프로세스가 유저모드에서 동작할 때에는 커널영역으로의 접근이 금지된다. 유저모드에서 실행중인 프로그램이 커널영역으로 접근을 시도하면 시스템에서 오류가 발생함을 알리고 접근을 봉쇄한다.

그러나 커널 모드에서 동작할 때에는 모든 영역의 접근이 허용된다. 스케줄러의 경우 커널영역에서 커널모드로 동작하지만 유저모드의 프로세스들을 스케줄링한다.

 

windows 운영체제 차원에서 제공하는 시스템 함스들은 커널의 구동을 필요로 한다. 따라서 이러한 함수들을 호출할 때마다 모드의 전환(커널모드 <-> 유저모드)가 발생한다. 이러한 모드의 전환은 시스템에 부담을 주기 때문에 상황에 따라 적절한 반영이 요구된다.

 

커널레벨 쓰레드와 유저레벨 쓰레드의 장점 및 단점

커널레벨 쓰레드의 장점 및 단점

장점 : 커널에서 제공해주기 때문에 안전성 및 다양한 기능을 제공받을 수 있다.

단점 : 커널에서 제공해 주는 기능이기 때문에 유저모드에서 커널 모드로의 전환이 빈번하게 일어난다. 따라서 성능의            저하를 발생시킬 수 있다.

 

유저레벨 쓰레드의 장점 및 단점

장점 : 커널은 쓰레드의 존재조차 모른다. 오로지 유저 모드로 동작하기 때문에 유저 모드에서 커널 모드로의 전환이              필요 없다. 때문에 성능이 좋다.

단점 : 예를 들어 하나의 프로세스 내에 3개의 쓰레드 A,B,C가 있다. 이중 A쓰레드가 시스템 함수를 호출했는데 커널에            의해서 블로킹 되었다. 이럴 경우 B, C도 실행되지 않는다. 운영체제는 프로세스의 존재만 알지 쓰레드의 존재를            모른다.때문에 A쓰레드가 속해 있는 프로세스 전부가 블로킹이 된다.

728x90
728x90

멀티 프로세스 기반 프로그램

서버와 접속용 클라이언트 프로그램이 있다고 가정할 때, 일반 사용자들은 서버 접속용 클라이언트 프로그램을 통해 서버에 접속하여 특정 서비스를 요청한다. 서버는 이러한 요구를 처리하기 위해서 요청이 있을때 마다 자식 프로세스를 생성한다. 동시에 둘 이상의 접속자에게 원할한 서비스를 제공해 주기 위함이다.

 

멀티프로세스 운영체제 기반 프로그램의 문제점과 새로운 제안

둘 이상의 실행 흐름을 위해 프로세스를 추가적으로 생성하는 작업은 매우 부담스럽다. 많은 수의 프로세스 생성은 빈번한 컨텍스트 스위칭(Context Switching)으로 이어져 성능에 영향을 줄 수 있다.

 

컨텍스트 스위칭 : 프로세스의 상태 정보를 저장하고 복원하는 일련의 과정이다.

 

컨텍스트 스위칭의 빈도수는 시스템에 따라 다르지만 못해도 초당 수십회 이상 발생한다. 따라서 이러한 컨텍스트 스위칭은 성능 저하의 원인이 된다.

 

하나의 프로그램 내에서 둘이상의 실행흐름을 두기 위해 등장한 것이 쓰레드이다. 쓰레드는 프로세스 처럼 각각 독립된 구조가 아니다. 즉 쓰레드들 사이에는 공유하는 요소들이 있다. 쓰레드는 공유하는 요소가 있어서 컨텍스트 스위칭에 걸리는 시간이 프로세스보다 짧다.

 

쓰레드를 생성할 때마다 해당 쓰레드의 스택은 새로 생성해주고, Code 영역, Data 영역, Heap영역은 부모 쓰레드와 공유한다.

 

쓰레드의 특성 1 : 쓰레드마다 스택을 독립적으로 할당해준다. 쓰레드는 Code영역, Data영역, Heap영역을 공유하고 Stack영역은 독립적으로 할당된다. 스택은 함수호출시 전달되는 인자, 되돌아갈 주소 값 및 함수 내에서 선언하는 변수등을 저장하기 위한 메모리 공간이다. 이 메모리 공간이 독립적이라는 뜻은 추가적인 실행 흐름을 만들 수 있다는 의미가 된다. 즉 실행 흐름의 추가를 위한 최소조건이 독립된 스택의 제공이다.

 

쓰레드의 특성 2 : 코드영역을 공유한다.

위 그림은 코드영역을 공유하는 것을 보여준다. 프로세스 main 하나와 쓰레드 main 두개가 있다. 결과적으로 프로그램의 실행 흐름은 총 3개가 된다.

 

쓰레드의 특성 3 : 데이터 영역과 힘을 공유한다.

쓰레드간에 힙과 데이터 영역을 공유하기 때문에 힙이나 데이터 영역을 통해 쓰레드 간에 서로 통신하는 것이 가능하다. 즉 전역변수와 malloc 함수를 통해서 동적 할당된 메모리 공간은 공유가 가능하다.

데이터 영역과 힙의 공유가 좋은것만은 아니다. 메모리 영역을 공유하다 보면 문제가 발생할 수도 있기 때문이다.

 

컨텍스트 스위칭이 빨라진 쓰레드

쓰레드 컨텍스트 스위칭은 프로세스 컨텍스트 스위칭에 비해 빠르다. 그 이유는 공유하는 영역이 많아서 이다.

또한 세부적인 요소들에는 레지스터들이 있다.

PC(Program Counter) : PC는 다음 실행해야할 명령어의 위치를 가리킨다. 쓰레드는 코드영역을 공유하기 때문에 PC는 컨텍스트 스위칭을 하더라도 영향이 없을거라고 생각하기 쉽다. 그러나 PC는 프로그램 실행 흐름과 관련이 있다. 쓰레드 별로 main 함수를 독립적으로 가지고 있고, 함수 호출도 독립적으로 진행되기 때문에 쓰레드별로 PC가 가져야할 값이 다르다. 그렇기 때문에 쓰레드 컨텍스트 스위칭에서 PC는 프로세스 컨텍스트 스위칭과 같다.

 

fp(Frame Pointer), SP(Stack Pointer) : 쓰레드별로 별도의 스택을 가지고 있기 때문에 fp, spp 의 경우도 컨텍스트 스위칭이 발생한다.

 

범용 레지스터 : 보통 연산을 위해 임시 데이터 저장소로 쓰인다. 연산은 프로그램 흐름에 따라 진행되므로 당연히 컨텍스트 스위칭 시 레지스터들도 컨텍스트 스위칭이 발생한다. 그러나 전역으로 선언된 변수를 할당 하기로 결정하였다면, 쓰레드의 컨텍스트 스위칭시 전혀 영향을 받지 않을 것이다.

 

Windows에서의 프로세스와 쓰레드

Window에서 프로세스는 쓰레드를 담는 상자이다. 실제 프로그램의 흐름을 형성하는 것은 쓰레드 이기 때문이다. window 운영체제에서 프로세스는 상태(Running, Ready, Blocked)를 지니지 않는다. 상태를 지니는 것은 프로세스가 아니라 쓰레드이다.

 

즉 쓰레드가 입출력에 관한 연산을 할 경우 Blocked상태에 놓이게 된다. 쓰레드는 입출력 연산이 끝나면 Ready상태가 되고, 그다음  Running 상태가 된다.

스케줄링 알고리즘 또한 프로세스가 아닌 쓰레드 기반으로 작동한다.

 

728x90
728x90

함수 호출규약 : 함수 호출시 인자를 전달하는 방식과 스택 프레임을 반환하는 방식

 

_cdecl, _stdcall + &

함수 선언부에 주로 존재하는 _stdcall 이라는 키워드는 함수 호출규약을 지정하는 것이다. _stdcall 호출 규약에 따라서 STDCallFunction함수의 호출과 반환을 처리하라는 뜻이다.

ex) int __stdcall STDCallFunction(int a, int b, int c);

 

WINAPI, APIENTRY, CALLBACK 등의 키워드는 아래와 같이 정의되어 있는 매크로이다.

#define CALLBACK  __stdcall

#define WINAPI  __stdcall

 

Windows 시스템 함수 선언에서는 키워드 __stdcall를 직접 사용하지 않는다. CALLBACK이나 WINAPI라는 또다른 이름을 부여해서 그 함수의 특성 파악에 도움을 주도록 하고있다.

 

ex)int CALLBACK EventRoutine(void);

CALLBACK은 실제로 __stdcall로 정의되어 있으므로, EventRoutine이라는 함수는 __stdcall호출 규약을 따를 것이다. 또한 콜백(CallBack)함수임을 파악할 수 있다. 함수 호출 규약이 선언되어 있지 않은 함수들은 디폴트 속성으로 선언된다.

 

콜백(CallBack)함수란, Windows 시스템에 의해 자동으로 호출되는 함수를 의미한다. 특정 상황에서 호출되어야 할 함수를 등록시키는 것이 가능한데, 이때 등록이 되는 함수를 가리켜 콜백 함수라 한다.

 

 

Calling Conventions

Segment

Word Size

Calling

Convention

Parameters

in rgisters

Parameter order on stack

Stack

Cleanup by

32bit

__cdecl

 

C

Caller

__stdcall

 

C

Function

__fastcall

ecx,edx

C

Function

__thiscall

ecx

C


Function

64bit

windows

(MS, Intel)

rcx/xmm0,

C

Caller

rdx/xmm1,

r8/xmm2,

r9/xmm3,

Linux, BSD

(GNU, Intel)

rdi, rsi,

C

Caller

rdx, rcx, r8

r9, xmm0-7

32비트 기반 함수 호출규약

__cdecl은 C/C++의 디폴트 호출규약이다. 인자 전달 방식은 C언어 스타일을 따르는데, C언어 스타일은 전달되는 인자가 스택에 쌓이는 방식을 의미한다. 반환 시에는 함수를 호출하는 호출자가 스택프레임을 반환한다.

__stdcall와 __cdecl의 차이점은 스택프레임을 반환하는 주체이다.

__stdcall은 호출된 함수 내에서 스택프레임을 반환하도록 정의되어 있다.

__cdecl은 호출자가 스택 프레임을 반환한다.

 

__fastcall은 말그대로 함수 호출을 빠르게 처리하기 위한 호출규약이다. __fastcall은 함수 전달인자를 두개까지 레지스터를 사용한다. 첫번쨰 전달인자는 ecx, 두번쨰 전달인자는 edx를 통해 저장된다. 두개가 넘어서면 스택을 사용한다. 이 호출규약에서 레지스터를 사용하는 것이 함수 호출이 빨라지는 근거가 된다.

 

64비트 기반 함수 호출 규약

Windows기반에서는 총 8개의 레지스터를 활용해서 전달되는 인자로 저장하게 되는데, 실제로 레지스터에 저장되는 전달인자 개수는 4개에 지나지 않는다. rcx/xmm0는 첫번째 전달인자가 rcx 혹은 xmm0레지스터에 저장된다는 것을 의미한다. 총 4개의 전달인자까지만 레지스터를 통해 처리한다.

Linux 또는 BSD에서는 최대 14개의 인자까지 레지스터를 통해 처리하는 경우도 있다.

728x90
728x90

코드 영역은 프로그램이 동작하기 위한 프로그램 코드(컴파일 된 명령어들의 집합)가 올라가는 위치이다. 프로그램을 실행시키면 위와같은 메모리 구조가 형성되고 코드 영역에, 실행되어야할 명령어들이 올라가서 순차적인 실행이 이루어지게 된다.

 

명령어의 실행은 세단계(Fetch, Decode, Execution)로 구분되어 진행된다. 이중 첫번째 단계는 Fetch인데 이것은 명령어를 CPU내부로 가져오는 단계이다. 컴파일된 프로그램 코드가 코드영역에 올라간 다음부터 Fetch, Decode되고 Execution되는 것이다.

 

명령어 길이가 4바이트라고 하고, 실행 중인 프로그램이 현재 1036번지에 있는 명령어라면, 다음 번에는 1040번지에 있는 명령어가 Fetch 되어야 한다.

 

CPU가 메모리 영역 중 스택을 컨트롤 하기 위해서 SP레지스터를 두었던 것처럼, 명령어를 순차적으로 fetch하기 위해서 프로그램 카운터라 불리는 "PC 레지스터"를 둔다.  CPU는 Fetch, Decode, Execution 과정을 계속해서 진행하도록 구현되어 있기 때문에, Fetch 연산이 일어날때마다 자동적으로 PC값이 증가한다.

 

프로그램 카운터(PC)

위 그림에서 IR Register를 보여주는데 IR Register는 명령어를 가져오기 위해서 사용되는 레지스터 이다.

 

 

코드영역에서 함수 호출

위그림의 오른쪽은 컴파일된 바이너리 코드가 실행을 위해 코드영역에 올라가있는 모습이다. 함수 호출시 실행 흐름의 이동은 Program Count PC에 의해 이루어진다. 함수 호출전 PC의 값(return address)을 스택에 저장해 두고 이후 함수 호출로 인해 이동해야할 주소값을 저장해두면 자연스럽게 실행의 위치는 이동하게 된다.

 

728x90
728x90

함수 호출 인자의 전달방식

전달되는 인자가 함수 내에서는 유효하고, 함수 호출이 끝나면 사라지는것으로 봐서 지역변수처럼 스택에 할당된다고 할 수 있다. 그러나 모든 전달인자들이 반드시 스택에 할당되는 것은 아니다. 성능향상을 위해 일부 전달인자들은 레지스터에 저장하는 경우도 있다.

 

PUSH & POP 명령어 디자인

"SP가 가리키는 현재 위치에 전달되는 인자값을 저장하고 나서, SP를 증가시켜 다음 메모리 주소를 가리키게 한다."

이와 같은 인자 전달 연산을 위해 명령어를 구성해 보자.

 

STORE 명령어는 레지스터에 저장된 데이터를 메모리에 저장하는 명령어이다.

STORE   대상(레지스터), 목적지(메모리 주소)

 

prob1) 명령어 조합을 통해서 숫자 7을, SP가 가리키는 메모리 위치에 저장하라.

1.숫자 7을 레지스터에 저장한다. 이후 레지스터를 STORE의 피연산자에 숫자 7의 값을 가지고 있는 레지스터를 넣을 수 있다.

ADD r1, 7, 0

 

이제 r1을 피연산자로 둘 수 있다.

 

2.SP가 주소 정보를 담고 있어야 한다. 이때 Indirect 모드를 사용한다.

STORE SP, 0x40 명령어를 통해 SP에 0x40을 대입한다.

 

3.Indirect모드를 통해 0x40번지를 참조하여 데이터를 저장할 수 있다.

STORE r1,[0x40]

 

"숫자 7을 SP가 가리키는 메모리 위치에 저장하라"의 명령어 구성은 

ADD r1, 7, 0

STORE sp, 0x40

STORE r1, [0x40]

으로 구성할 수 있다.

위과정 이후 반드시 SP레지스터 값을 증가시켜야 한다. 다음에 들어오는 데이터를 저장하기 위해서다.

ADD sp, sp, 4

최종적인 명령어 조합은 아래와 같다.

 

ADD, r1,7,0

STORE sp, 0x40

STORE r1, [0x40]

ADD sp, sp, 4

 

PUSH & POP

왼쪽 그림은 명령어 PUSH이다. 데이터를 스택에 넣고자 하는 경우 사용한다.

ex) "PUSH 0x02"   or    "PUSH r1"

SP값을 참조하여 해당 위치에 데이터 0x02 or r1의 값을 저장하고 SP의 값 또한 자동으로 증가시키는 명령어이다.

 

오른쪽 그림은 명령어 POP이다. 스택에 가장 마지막에 들어간 데이터를 꺼낸다는 것은 메모리에서 삭제함을 의미한다. 즉 SP를 감소시키는 것이다. 32bit 환경에서 SP의 증가와 감소는 4바이트 단위로 이루어진다.

POP 명령어와 같은 일을 하는 명령어는 다음과 같이 구성할 수 있다.

"ADD sp, sp, -4"   or "SUB  sp, sp, 4"

 

C코드를 어셈블리 코드로 바꾸기

1.PUSH fp를 통해 이전 스택 프레임 포인터를 스택에 저장한다.

2. ADD fp, sp, -4는 1.의 PUSH fp를 통해 SP가 4 만큼 증가되어 있으므로 -4한 값을 fp에 저장한다.

3.PUSH 7

4. PUSH 8

3.PUSH 7과 4.PUSH 8은 각각 함수의 전달인자로 넣어준다.

728x90
728x90

ATPCS(ARM-Thumb Procedure Call Standard)

이는 함수의 전달인자와 리턴 어드레스(함수 호출이 완료되고 나면 돌아갈 주소)를 레지스터에 저장하기로 결정하고, 저장 방식에 대한 표준을 정의한 것이다. 이 표준을 고려하여 ARM코어의 레지스터들도 디자인 되어 있고, ARM컴파일러도 이 표준에 맞게 바이너리 코드를 생성하도록 디자인 되어있다.

 

스텍 프레임(Stack Frame)구조

함수 호출 과정에서 할당되는 메모리 블록(지역 변수 선언으로 인해 할당되는)을 가리켜 스택 프레임이라 한다. 위 그림을 보면 main함수에 변수 a와 b가 선언되어 있다. 따라서 스택에도 a, b가 할당되어 main 함수의 스택 프레임을 구성한다.

 

return에 의해 함수 호출이 완료되면 해당 함수의 지역변수에는 접근이 불가능하다. 할당되었던 메모리가 반환되었기 때문이다.

 

SP(Stack Pointer)레지스터

지역변수를 저장하는 메모리 공간을 스택이라 이름 붙이는 이유는 메모리의 구조적 특성(First In, First Out)때문이다.

스택 프레임은 가장 먼저 할당되면 가장 나중에 반환된다. 그리고 가장 나중에 할당되면, 가장 먼저 반환된다.

 

스택에 데이터를 쌓거나 반환하기 위해서는 현재 어느 위치까지 데이터를 저장했는지 기억해야 한다. 이것을 하는 것이 CPU내에 존재하는 SP(Stack Pointer)이다.

지역 변수는 a,b,c ... 순으로 스택에 할당된다. SP 레지스터 값은 이렇게 변수가 하나씩 할당될 때마다 증가한다. 증가하면서 다음 변수가 할당될 메모리 위치를 가리킨다. 함수가 종료할 때에도 SP레지스터 값을 이동시켜야 한다. 호출된 함수가 종료될 경우 그 함수 내에서 선언된 변수들을 동시에 모두 반환해야 하기 때문이다.

 

함수가 종료되고 스택 프레임 단위로 SP를 아래로 이동시킬 때는 얼마만큼 SP를 이동시켜야 하는지 알 수가 없다.

이를 해결해 주는 것이 프레임 포인터 레지스터이다.

 

프레임 포인터(Frame Pointer) 레지스터

되돌아갈(함수 호출 이전의) SP 위치를 저장해 놓으면 함수 반환시 스택 프레임 단위로 SP위치 조절이 가능하다. 이 역할을 하는 레지스터를 가리켜 fp(Frame Pointer)레지스터라 한다.

 

sp레지스터에 저장된 값을 fp레지스터에 저장하는 상황을 보여준다. fct1에서 많은 변수를 선언하더라도 fp에 저장된 값을 참조해서 fct1함수 호출 이전 위치로 sp를 이동시킬 수 있다.

스택프레임 포인터를 이용하여 정확히 함수의 스택 프레임만 반환할 수 있게 된 것이다.

 

 

fp 레지스터의 문제점

위처럼 두번쨰 함수 호출시 sp레지스터가 이전 fp값을 덮어버리기 때문에 문제가 발생한다. fct1의 값이 사라져 레지스터 값을 참조할 수 없게 된다.

 

스택에 저장하자, 프레임 포인터(Frame Pointer)

여러 함수를 호출할 때 fp를 덮어쓰는 문제점을 해결하는 방법은 덮어 쓰기 전에 fp에 저장된 값을 다른곳에 저장해 두는 것이다. 즉 함수 호출이 일어날 때마다 fp레지스터에 저아되어 있는 값을 스택에 저장하는 것이다. 그리고 이후에 새로운 값으로 fp레지스터를 채운다.

1.fct2함수가 호출되기 직전에 sp레지스터에는 주소값 20이 들어가 있다. 현재 스택 주소를 가리키는 것이다.

2.fct2함수가 호출되기 직전에 fp레지스터에는 주소값 8이 들어가 있다. fct1의 스택 프레임 포인터이다.

3.fct2 함수가 호출되면서, fp레지스터에 저장된 값(주소값 8)을 현재 sp레지스터가 가리키는 위치 20번지에 먼저 저장한다. 그다음 fp레지스터에 sp레지스터 값 20을 저장한다.

4.fct2 함수 호출이 완료되어 반환한다면, fp레지스터에 저장된 값을 참조해서 sp레지스터 값을 20으로 변경한다. 이는 fct2함수의 스택 프레임을 날리는 것이다.

5.현재 sp레지스터가 가리키는 위치(주소 20번지)에 저장되어 있는 값을 fp레지스터에 옮겨다 놓는다. 이로써 fct1함수 호출이 완료되는 상황에서 sp의 위치를 8번지에 가져다 놓을 수 있게 된다.

 

728x90

+ Recent posts