Computer Science 기본 지식/컴퓨터 구조

[컴퓨터 구조] 2. 하드웨어 프로시저 지원 / 프로그램 번역과 실행

로파이 2021. 4. 15. 15:06

프로시저

 - 제공되는 인수에 따라 특정 작업을 수행하는 서브 루틴

 

RISC-V는 프로시저를 호출할 때, 다음 레지스터 할당 관례를 따른다.

x10-x17 : 전달할 인수와 결과값을 가지고 있는 인수 레지스터 8개

x1: 호출한 곳으로 되돌아가기 위한 복귀 주소를 가지고 있는 레지스터 1개

x0: 항상 0 으로 유지된다.

 

- 지정된 주소로 분기 Jump and link

// 다음 명령어는 지정된 주소로 분기하며 동시에 다음 명령어 주소를 목적지 레지스터 rd에 저장한다.

 

ja1 x1, ProcedureAddress

 

x1에 저장되는 주소는 루틴 종료 후 복귀할 주소가 담긴다. link는 프로시저 종료 후 올바른 주소로 돌아올 수 있도록 연결한다는 의미이다.

 

- 프로시저의 복귀

jalr x0, 0(x1)

 

x1에 저장되어 있는 주소로 분기하라는 의미이다. 호출 프로그램(caller)은 x10-x17에 전달할 인수값을 넣은 후 jal x1, X 명령을 이용해서 프로시저 X(피호출 프로그램)로 분기한다. 피호출 프로그램은 계산을 끝낸 후 계산 결과를 인수 레지스터에 넣은 후 jalr x0, 0(x1) 명령을 시행하여 복귀한다.

 

내장 프로그램은 현재 수행 중인 명령어를 기억하고 있는데 이 레지스터를 명령어 주소 레지스터 혹은 프로그램 카운터 PC라고 한다. jal 명령어는 프로시저에서 복귀할 때 다음 명령어를 실행하도록 PC+4를  복귀 주소를 저장하는 레지스터(x1)에 저장한다.

 

레지스터 스필링

호출 프로그램의 레지스터를 보존하기 위해 스택에 저장해두고 피호출 프로그램 종료시 스택에 있던 레지스터를 다시 꺼내오게 된다.

스택 포인터는 스택의 맨 위를 가리키는 값이다. 저장 (Push) 혹은 꺼내질(Pop) 때마다 8씩(더블 워드) 조정된다. 

C 프로시저 예제

long long int leaf_example(long long int g, long long int h, long long int i, long long int j)
{
    long long int f;
	
    f = (g+h) - (i+j);
    return f;
}

g,h,i,j는 인수 레지스터 x10, x11, x12, x13에 해당하고 f는 x20에 해당한다.

 

임시 레지스터 x5, x6와 f(x20)를 사용하기위해 기존 레지스터 값을 스택에 저장해두어야한다.

  • addi sp, sp, -24 // 스택 포인터를 3 x 8 만큼 주소를 감소 시킨다.
  • sd x5, 16(sp)
  • sd x6, 8(sp)
  • sd x20, 0(sp)

다음은 함수 내용이다.

  • add x5, x10, x11 // 임시 레지스터 x5 = (g+h)
  • add x6, x12, x13 // 임시 레지스터 x6 = (i+j)
  • sub x20, x5, x6   // 레지스터 x20 = x5 - x6
  • addi x10, x20, 0 // 결과를 저장

 호출 프로그램으로 돌아가기 전에 저장해 두었던 값을 스택에서 꺼내 레지스터를 원상복구 한다.

  • ld x20, 0(sp)
  • ld x6, 8(sp)
  • ld x5, 16(sp)
  • addi sp, sp, 24 // 스택 포인터를 조정한다. 
  • jalr x0, 0(x1) // 복귀 주소를 사용하는 분기 명령으로 끝난다.

- RISC-V는 레지스터 19개를 두 종류로 나눈다.

  • x5-x7, x29-x31: 피호출 프로그램이 값을 보존하지 않는 임시 레지스터
  • x8-x9, x18-x27: 프로시저 호출 전과 후의 값이 같게 유지되어야 하는 변수 레지스터 12개

- 프로시저 호출시 Caller에서 보존되는 값들

프로시저 호출시 보존되는 값들과 아닌 값들

프로시저를 호출하면 Caller는 스택 포인터를 조정하여 보존해야하는 레지스터들과 함수 인수(arguments), 지역 변수, 복귀해야하는 주소, 프레임 포인터스택에 저장해둔다. Callee는 스택 포인터보다 높은 주소에 있는 데이터를 쓰지 못하게 하여 Caller가 저장한 영역을 보호할 수 있다. Callee가 복귀주소로 돌아갈 때 레지스터를 복구하고 스택 포인터를 다시 조정하게 된다.

 

프로시저 호출 이후 스택의 상황 (호출 이전 -> 호출 이후 -> 복귀)

레지스터보다 큰 구조체나 배열, 지역변수를 저장하기 위해 스택 공간을 사용하기도 한다. 프로시저의 보존 레지스터와 지역변수를 가지고 있는 스택 영역을 프로시저 프레임이라고 한다.

프레임 포인터는 이 프로시저 프레임의 시작 주소를 가리키고 스택 포인터는 프레임의 끝, 스택의 맨 위 주소를 가리키게 된다. 프레임 포인터는 베이스 주소의 역할을 하므로 지역 변수 참조가 용이해진다.

 

프로그램 번역과 실행

C언어 번역 계층

  1. 상위 수준 프로그램은 컴파일러에 의해 어셈블리 언어로 컴파일 된다.
  2. 다시 어셈블러에 의해 기계어 형태의 목적 모듈이 생성된다.
  3. 링커는 여러 모듈과 라이브러리 루틴을 연결하여 모든 외부 참조를 해결한다.
  4. 로더가 기계어 코드를 적절한 메모리 위치에 넣어서 프로세서가 실행할 수 있게한다.

- 컴파일러

컴파일러는 C 프로그램을 어셈블리 언어 프로그램으로 바꾼다.

 

- 어셈블러

어셈블리 프로그램을 기계어로 번역한다. 어셈블리 언어 프로그램을 목적 파일(object file)로 바꾸며,

목적 파일에는 기계어 명령어, 데이터, 명려어를 메모리에 적절히 배치하기 위해 필요한 각종 정보들이 혼합되어 있다.

 

UNIX 시스템에서 목적파일의 구성

- 목적 파일 헤더: 목적 파일을 구성하는 각 부분의 크기와 위치를 서술한다.

- 텍스트 세그먼트: 기계어 코드가 포함된다.

- 정적 데이터 세그먼트: 프로그램 수명 동안 할당되는 데이터가 들어가 있다.

- 재배치 정보: 프로그램이 메모리에 적재될 때 절대 주소에 의존하는 명령어와 데이터 워드를 표시한다.

- 심벌 테이블: 외부 참조와 같이 아직 정의되지 않고 남아 있는 레이블들을 저장한다.

- 디버깅 정보: 각 모듈이 어떻게 번역되었느지에 대한 간단한 설명이 들어 있다.

 

- 링커

각 프로시저를 따로따로 컴파일, 어셈블리 하는 것이다.

 

링커의 동작 3단계

1) 코드와 데이터 모듈을 메모리에 심벌 형태로 올려 놓는다.

2) 데이터와 명령어 레이블의 주소를 결정한다.

3) 외부 및 내부 참조를 해결한다.

 

링커는 컴퓨터에서 실행될 수 있는 실행 파일을 생성한다.

- 목적 파일 링크 과정

목적 파일 구성 예시

프로시저 A와 프로시저 B의 재배치 정보를 참고해보면 각각 A는 X라는 전역 변수와 B프로시저의 주소를 알아야하고 B는 Y라는 전역 변수와 A프로시저의 주소를 알아야한다. 이는 각 프로시저 명령어 본문에 사용되고 있기 때문이다.

 

텍스트 세그먼트의 시작 주소는 0000 0000 0040 0000이며 데이터 세그먼트의 주소는 0000 0000 1000 0000이다.

프로시저 A의 텍스트 세그먼트 사이즈100바이트 이므로 B의 텍스트 세그먼트 시작 주소0000 0000 0040 0100이다. A의 데이터 세그먼트 사이즈는 20바이트 이므로 B의 데이터 세그먼트 시작 주소 0000 0000 1000 0020이다.

 

--- 심볼릭 링크 해결 과정

1. A의 프로시저 명령어 중 jal x1, 0에서 B의 프로시저 시작 명령어 주소로 가야하기 때문에 B의 텍스트 세그먼트 (명령어가 저장되어 있는 영역)의 시작 주소는 상대적 주소로 계산되어 (40 0100 - 40 0004) = 252(10진수)가 기입된다. B의 프로시저 명령어 중 A의 프로시저 시작 주소로 가는 부분은 마찬가지로 -260으로 채워진다.

2. ld 명령어들을 보면, x3를 베이스 레지스터로 사용하고 있다. x3에 데이터 세그먼트 주소가 저장되어 있다면 첫번째 전역변수 X의 주소는 x3와 같기 때문에 0의 오프셋을 Y 전역 변수는 0x20 = 32 오프셋 주소를 가지게 된다.

 

- 로더

실행 파일은 디스크에 있으므로 운영체제가 이를 읽어서 메모리에 넣고 시작시킨다.

1. 실행 파일 헤더를 읽어서 텍스트와 데이터 세그먼트의 크기를 알아낸다.

2. 텍스트와 데이터가 들어갈 만한 주소공간을 확보한다.

3. 실행 파일의 명령어와 데이터를 메모리에 복사한다.

4. 주 프로그램에 전달해야할 인수가 있으면 이를 스택에 복사한다.

5. 레지스터를 초기화하고 스택 포인터는 사용 가능한 첫 주소를 가리키게 한다.

6. 기동 루틴을 분기한다. 기동 루틴에서는 인수를 인수 레지스터에 넣고 프로그램의 주 루틴을 호출한다. 주 프로그램에서 기동 루틴으로 복귀하면 exit 시스템 호출을 사용하여 프로그램을 종료시킨다.

 

- 동적 링크 라이브러리 (Dynamic Link Library)

프로그램 실행 전에는 라이브러리가 링크되지도 않고 적재되지도 않는다. 대신 프로그램과 라이브러리 루틴은 전역적 프로시저의 위치와 이름에 대한 정보를 추가로 가지고 있다.

 

동적 링크 과정

처음 라이브러리 루틴을 호출하게 되면 더미 엔트리를 호출하고 간접 분기를 따라간다. 이는 동적 링커/로더로 분기하고 원하는 루틴을 찾아 재사상한 다음 이 루틴을 가리키도록 간접 분기의 주소를 바꾼다. 이 후 해당 루틴을 다시 호출하면 간접 분기 하나로 찾을 수 있다.

 

참고 : Computer Organization and Design RISC-V edition