시스템프로그래밍

[시스템프로그래밍] 컴파일과 런타임 스택

bebeghi3356 2024. 12. 1. 20:28

어셈블리 언어(.asm)

 

"우리가 고급 언어로 프로그램을 만들면, 컴파일러가 이를 해석해서 어셈블리어로 만듭니다.

이 어셈블리어는 또 다시 어셈블러를 통해 기계어로 번역됩니다."

 

라고 보통 설명하지만, 대부분의 경우에는 고급 언어 -> 중간 언어 -> 어셈블리어 = 기계어라고 보면 될 것 같습니다.

 

어셈블리어는 컴파일러마다 다르지만, 지금 듣는 수업 내용은 c언어로 설명하므로 앞으로 내용은 c언어를 기준으로 한다.

 

c언어 실행과정

C 프로그램이 컴파일이 되어 어셈블리어 명령어가 된 모습을 볼 수 있습니다.

 

다만 이때 중요한 건 Linker라는 특이한 애가 존재하고, 얘가 다른 라이브러리와 내가 만든 어셈블리어를 연결시켜 준다는 점이 중요합니다.

 

위에서 설명했다시피, 고급언어 -> 기계어는 굉장히 시간이 오래 걸리기 때문에,

이미 어셈블리어나 기계어로 번역해놓은 라이브러리가 있다면 그것을 이용하는 것이 속도 면에서 매우 유리합니다.

 

그렇기 때문에 C를 변환한 어셈블리어는 어셈블러를 통하여 obj 파일이 되고, 

이때 stdio.h 같은 라이브러리 파일은 이미 obj 파일로 존재하기 때문에, 이를 합쳐주는 것이 Linker의 역할입니다.

 

어셈블리어(Assembly language)는 기계어와 1:1로 대응되는 저급 프로그래밍 언어입니다. 보통 어셈블리어는 어셈블리라고도 하고, 일반적으로는 ASM이나 asm이라는 단어로 약칭되기도 합니다. 어셈블리어는 기계어나 프로그래밍 언어와 마찬가지로 컴퓨터에서 직접 실행될 수 있는 형태의 코드를 작성할 수도 있습니다.

 

세그먼트

 

  • _BSS SEGMENT : 초기화되지않은( 또는 0으로 초기화된) 전역변수
  • _DATA SEGMENT : 초기화된 전역변수
  • CONST SEGMENT : 문지열(string) 등의 상ㄷ수
  • _TEXT SEGMENT : 실행코드
  • 지역변수는 ? >>

! BSS세그먼트 : 초기화가 안된 전역변수 , DATA 세그먼트 : 초기화된 전역변수

! 지역변수는 해당 함수가 실행하는 동안만 할당하고, 함수에서 return하면 반납하는 시스템이 필요함 >> 스택(stack)

 

스택(stack)

~ 자료구조 내용

 

* 스택 오버플로우 : 메모리 구조 중 스택(stack) 영역에서 해당 프로그램이 사용할 수 있는 메모리 공간 이상을 사용하려고 할 때 발생합니다

 

함수의 return address 

: 컴퓨터 프로그래밍에서 함수 호출이 끝난 후 실행이 다시 돌아갈 지점(주소)를 의미한다.

함수 호출과 복귀의 흐름을 제어하는 데 중요한 역할을 한다.

만약 스택이 sub2 > sub 1 > 주 프로그램 순서로 쌓여있다고 생각하면

 

  • 작동 원리

1. 함수 호출

- 함수가 호출되면 현재 실행 중인 명령의 다음 주소(호출한 지점의 다음 명령어의 주소)가 스택에 저장된다.

- 이 주소를 retrun address라고 함

- 함수 호출시에 스택에 할당되는 영역을 스택 프레임이라고 한다.

2. 함수 실행

- 함수 실행 그리고 스택에 데이터를 추가하여 지역변수나 매개변수 처리

- 스택 프레임에는 retrun address, 지역변수, 매개변수가 포함된다고 볼 수 있다.

3. 함수 종료

- 실행 완료되면 스택에 저장된 return address를 가져와 해당 주소로 복귀한다 ( 스택은 나중에 넣은게 먼저 나오므로 가장 최근 호출된 함수의 리턴 어드레스를 가장 먼저 꺼낼 수 있다 !!!)

-  이렇게 함으로써 호출한 코드의 다음 명령어를 실행할 수 있다.

 

재귀호출

: 재귀호출이란 함수 내부에서 함수가 자기 자신을 또다시 호출하는 행위이다. 재귀 호출은 자기가 자신을 계속해서 호출하므로 끝없이 반복되므로 함수 내에서 중단하도록 하는 명령문을 반드시 포함해야한다.

( 명령문or 조건문을 포함하지 않으면 실행 직후 스택 오버플로우에 의해 프로그램이 종료된다)

 

1. 재귀호출시 스택 구조

재귀 호출이 이루어질때마다 함수의 스택 프레임이 새롭게 생성된다.
각 호출은 고유한 스택 프레임을 가지며, 지역 변수도 호출된 각 함수의 스택 프레임 내에서 독립적으로 존재한다.

 

 

2. 재귀호출의 반환제어 

재귀 호출에서는 함수가 호출될 때마다 스택에 함수의 반환 주소와 현재 상태(지역변수, 매개변수 등)가 저장된다.
함수가 종료되면, 스택에서 pop하여 저장된 반환 주소로 돌아가게 된다. 
따라서 올바르게 동작하려면 재귀 호출된 횟수만큼 반환을 처리해야 한다.

 

 


1. 스택에 저장되어 있는 값들 중간에 값을 push하는 것이 불가능하다 

: LIFO

 

2. 스택에서 새로 삽입되는 값은 sp가 가리키는 top 위치에 삽입된다.

: stack pointer의 정의는 top의 주소를 가리키는 포인터이다.

 

3. 스택은 지역변수 할당에 사용되는 자료구조이다.

: 스택은 return address, 지역변수, 매개변수가 포함된다.

 

4. 스택은 push되는 순서대로 pop되는 자료구조이다. (X)

: 스택은 맨위에서 부터 pop되니까 push되는 순서대로 pop이 되지 않는다

 

5. 아래와 같은 함수호출 관계에서 반환주소(return address)를 저장한 스택이 다음과 같을 때 현재 어느 부분을 실행 중인지 설명하시오.

(스택 표에서 위쪽이 스택의 top 위치임)

 

   
   
 
261 (top)
211

 



sub2 실행중....인데 왜?ㅅㅂ

 

 

6. 재귀호출로 자신을 계속 호출하는 경우 재귀호출의 횟수 만큼의 반환문이 실행되어야 할 수도 있다. 이 횟수 제어를 정확히 할 수 있는 방법을 잘 설명한 것은?

 

 

 

- 호출할 때마다 스택에 반환주소를 push하고 그 횟수만큼 pop함으로써 제어한다.

1. 위에서 재귀 호출이 발생할떄마다 함수의 상태와 반환 주소를 스택에 저장한다고 했지? 반환 시에는 스택에서 꺼내고 다시 실행해.
2. 호출된 함수가 실행을 마치게되면 호출한 함수로 돌아가기 위해 스택에 저장된 반환 주소로 복귀해.
3. 재귀 호출이 중첩된 경우? 이때 반환 형식은 LIFO방식으로 이루어져.

 

- 카운터를 스택에 저장하여 카운터를 증감하며 제어한다.(X)

1. 카운터 : 재귀호출의 횟수를 추적하기 위한 변수, 반환 흐름 자체를 나타낸다.
(예시)
def recursive_function(counter):
if counter == 0:     //재귀 호출의 흐름을 추적할 수 있게 해줌
return
print(f"현재 호출 깊이: {counter}")
recursive_function(counter - 1)
print(f"반환 중: {counter}")
2. 틀린이유 :  재귀 호출 횟수를 추적할 때 카운터를 사용할 수 있지만, 반환 흐름 자체를 제어하지 않는다.

 

- 재귀호출을 여러번 하여도 반환은 한번만 하면 되므로 제어가 필요 없다.

1. 틀린이유 : 재귀 호출이 발생하면 호출된 모든 함수는 각자 반환 과정을 거쳐야 합니다. 반환을 한 번만 하면 논리적 오류가 발생합니다.

 

7. 재귀호출로 자신을 계속 호출하는 경우 함수의 지역변수에 대한 설명이다.

; 재귀호출로 함수가 자신을 계속 호출하는 경우, 함수의 지역변수는 호출마다 스택에 독립적으로 할당된다.

 

- static 속성으로 처리되어 여러 번 호출되어도 같은 메모리 영역을 공유한다.(X)

틀린이유 : 지역변수가 static으로 선언된 경우에만 메모리 영역을 공유한다. 일반적인 지역변수는 호출 시마다 새로운 공간을 할당받는다.

 

- 재귀 호출되는 함수에서는 지역 변수를 사용하지 못한다.(X)

틀린이유 : 재귀 호출에서도 지역 변수를 사용할 수 있으며, 호출마다 독립적인 메모리 공간을 가집니다.

 

- 호출 때마다 서로 다른 지역 변수 영역이 스택에 할당된다.

각 호출은 독립된 지역 변수를 스택에 생성합니다.

 

8. 스택에 서브함수의 지역변수가 할당되는 시점은?

: 호출되어 함수가 시작되는 시점 ( 호출되면 스택프레임이 생성되고 지역변수가 스택에 할당됨)

 

다른 시점에서는 어떤 작업이 이뤄질까?

  • 컴파일되는 시점: 컴파일 시점에는 프로그램 코드가 바이너리로 변환될 뿐, 실행 중 메모리 할당은 이루어지지 않습니다.
  • 전처리가 끝난 시점: 전처리 과정은 컴파일 전 소스 코드를 변환하는 단계일 뿐, 메모리 할당과는 무관합니다.
  • 주 프로그램의 실행이 시작되는 시점: 프로그램이 시작될 때는 전역 변수나 정적 변수 등이 초기화될 수 있지만, 함수 호출이 없으면 지역 변수는 생성되지 않습니다.
  • 스택에 서브함수의 반환주소가 저장되는 시점 : 함수 호출문이 실행될 때

9. 스택에 여러 개의 반환 주소가 저장되어 있다. 가장 먼저 pop되는 반환 주소는 ?

: 가장 마지막으로 호출된 위치의 반환주소

 

10. 다음 프로그램에는 data라는 id를 갖는 변수가 3개 존재한다. 함수 sub()에 정의된 변수data에 대한 설명으로 잘못 된 것은?

int  data=10;  //전역변수

void sub();

void  main()

{

  int  data=20;  //지역변수

  data ++;

  sub(); 

  data ++;    

}

void sub()

{

  int  data=30;  //지역변수

  data ++;

}

//같은 이름의 변수가 지역적으로 선언되면, 해당 지역 변수는 전역 변수보다 우선적으로 사용된다.