강의/system programming

[system programming] Computer Program의 표현과 실행(Basic of Assembly-3, function call)

하기싫지만어떡해해야지 2025. 3. 30. 17:08

본 게시글은

서울대학교 데이터사이언스대학원 정형수 교수님의 

데이터사이언스 응용을 위한 시스템 프로그래밍 강의를

학습을 목적으로 재구성하였습니다


오늘은 컴퓨터 프로그램의 표현과 실행 3번째 시간

이번 수업의 주요 내용은 function call을 할 때

컴퓨터 프로그램은 내부에서 어떤 작업을 하며

어떤 방식으로 function을 수행시킬까

그리고 이를 assembly로 어떻게 나타낼까

이다

 

 

매번 수업시간마다 나오는 지겨운 그림이다

 

CPU 아키텍쳐는 크게 봤을 때 state machine과도 같다고한다

하나의 instruction이 trigger가 되어서

instruction이 수행될 때마다 CPU의 state가 바뀌기때문에 그렇다고 한다

 

 

 

 

우리 수업이나 다른 교과서에서는

통상적으로 stack memory를 거꾸로 뒤집어서 본다고한다

 

%rsp는 stack의 주소를 가리키는 stack pointer인데

보통 %rsp는 stack 영역의 가장 마지막을 가리키고

마지막일수록 주소값은 낮아진다

그래서 bottom of stack이라고 하면

보통 가장 윗부분을 얘기한다

 

heap은 우리가 c언어에서 자주 사용하는

malloc과 free를 통해서 control 할 수 있지만

stack은 assembly로 코딩을 하지 않는 이상

직접 컨트롤이 불가능하다

 

아무튼 이런 stack pointer인 rsp를

이동시키는 것을 stack 자료구조와 동일하게

push와 pop이라고 지칭한다

 

가장 먼저 push를 살펴보자

자료구조 stack을 배우면 나오는 push와 유사하다

 

assembly에서는 pushq Src와 같이 사용하는데

예를 들어

pushq %rax와 같은 assembly 코드가 들어오면

%rsp를 감소시키고 해당 주소에

%rax의 값을 저장한다

 

 

 

그 다음으로 pop을 살펴보자

assembly로는 popq Dest와 같이 사용하는데

stack의 가장 위값을 꺼내는 동작이다

 

예를 들어 popq %rdx와 같은 assembly code가 있다면

stack의 가장 윗 값을 가져와서

%rdx에 넣어달라는 뜻이 된다

그 후 %rsp를 한 칸 증가시킨다

 

이런 stack memory의 push와 pop은

runtime procedure를 수행할 때 매우 중요한 역할을 한다

 

 

 

push와 pop을 정리하면 이렇다고 한다

 

pushq %rbp는

subq $8, %rsp -> movq %rbp (%rsp)와 동일하고

popq %rdx는

movq (%rsp), %rdx -> addq $8, %rsp와 동일하다

 

천천히 생각해보면 금방 이해할 수 있다

push같은 경우는 주소값을 먼저 한칸(8 bytes) 내린 뒤

(subq $8, %rsp)

해당 주소값에 %rbp의 값을 넣어주는 것이다

(movq %rbp, (%rsp))

 

반대로 popq는

먼저 %rsp에 있던 값을 %rdx로 옮긴 뒤

(movq (%rsp), %rdx)

%rsp의 주소값을 한 칸(8 btyes) 올려주는 것이다

(addq $8, %rsp)

 

 

여기서부터 본격적으로 이번 수업에서

배워 볼 내용이라고 한다

 

우리가 이 수업에서 배우고있는 구조인

x86-64는 C언어에서 procedure들을 어떻게 호출할까?

쉽게 말하면 c언어로 함수를 작성하고 실행시키면

함수의 코드부분을 어떻게 가져와서

지금까지 실행시키고 있던 것은 어떻게 저장하며

함수는 어떻게 실행시키고

함수가 다시 끝난 후 어떻게 프로그램을 계속 이어나갈까?

 

교수님께서 간단히 전체 내용을 미리 설명해주셨는데

현재 내가 동작하고 있다가 function call이 들어왔고

그럼 그 function call을 수행하기 위해서

그 function의 코드로 넘어가야한다

 

그러면 우리가 수행해줄 instruction이 바뀌는 것이니

instruction pointer가 바뀌어야한다

function이 수행될 때마다 rsp를 갖고

그 함수 내부에서 local variables들과 

이것저것 필요한 것들을 저장하고 rsp를 내려준다

그런데 rsp가 내려갈 때 해당 함수를 실행하기 이전에

우리가 지금까지 저장해놨던 것들이

모두 사라지면 안된다

내가 지금까지 동작하고있었던 일련의 runtime semantic이

어딘가에 저장이 다 되어있어야한다

한 마디로, function call 하기 직전의 상태를

저장을 해야한다는 것이다

그래야 function 수행이 끝나면 다시 이전 상태로 돌아가서

작업을 계속 진행할 수 있는 것이다

 

이 이전상태에 대한 저장을 stack에 해준다고 한다

 

어떤 함수가 정의되어있으면

해당 함수에 대한 지역 변수나 이런저런 데이터들이

어떤 stack frame에 저장이 되어있을텐데

함수를 수행하고 다시 이전 상태로 돌아갈 곳인

return address가 이 함수의 stack frame의

가장 밑바닥에 저장되어있다

 

그럼 함수를 수행한 뒤

해당 return address를 보고 jump하는 것이다

 

예전에는 return address를 해킹하는 방법이 있었다고한다

그래서 요즘은 return address에 접근하는 것을

막고 있다고 한다

 

교수님께서 전반적으로 쭈루룩 설명해주신 것을

글로 담으려다보니 내용이 좀 부자연스러워진 것 같다

더 자세한 내용은 뒤에 나오니 차근차근 알아보도록하자

 

 

 

위 ppt를 보면 mult2라는 함수가 있고

multstore이라는 함수를 수행하면

mult2이라는 함수가 수행이 되어야한다

 

그럼 multstore 실행 중 mult2 함수 부분이 나오면

call을 해주면서 address function label이 들어가는데

이게 바로 return address이고

함수 수행이 끝난 뒤 다음으로 실행해야할 Instruction이

담겨있는 주소값이다

 

그렇다면 우리가 첫번째 시간에 배웠을 때

CPU에서 현재 실행되는 주소값을 다루는 레지스터를

PC(Program Counter)라고 배웠다

x86-64에서 PC의 역할을 하는 레지스터는 %rip이므로

해당 주소를 %rip에 넣어버리고

%rip register에 들어가면 해당 레지스터는

그 값을 메모리 주소로 파악하고

거기에 담겨있는 instruction을 자동으로 fetching해서 온다

 

 

그럼 이 함수를 수행하는

assembly code를 살펴보자

 

multstore을 수행하면서

rbx를 push하고

dest를 save한 뒤 mult2를 call해준다

그러고 dest의 주소값에 계산 결과를 세팅해준다

 

밑의 mult2도 유사한 방식으로 수행하고 있는 것을 확인할 수 있는데

argument가 a와 b 두개라도

두 개 다 저장하지 않고 a의 값을 b에 담아 놓은 다음

b에서 그냥 계산을 수행하고

새로운 memory에 access하지 않고 바로 ret를 해준다

 

이런 식의 최적화는 우리가 하는건 아니고

compiler가 알아서 해준다고한다

memory access를 하면 속도가 저하되므로

어떻게 하면 최대한 stack의 memory에 access하지 않고

해당 instruction을 수행할 수 있을지를

컴파일러가 계속 고민하고 optimizing을 해준다

 

 

 

passing control을 해주기 위해서

프로그램은 call과 ret instruction을 이용한다

 

 

 

function을 call할 때

push return address on stack을 해준다

위 ppt의 예시같은 경우

call 이후의 instruction인 400549가

retun address on stack에 저장될 것이고

함수 수행을 위해 400550으로 이동해서

해당 함수의 instruction을 수행해준다

그러고 mult2에서 ret하면 이전에 저장했던

return address인 400549로 이동하는 것이다

 

 

 

%rip에 400549가 들어가서 해당 함수 실행이 끝나면

400549에 있는 instruction을 fetching해서

다시 수행하게 된다

 

 

 

몇 번째 input argument부터 stack의 어디에

이렇게 저장해놓자라는 일종의 규칙이

다 정해져있다고 한다

 

이게 CPU 아키텍처에서 calling convention이라고 불리는 것이라고한다

 

 

 

calling convention은 이런식이다

함수의 argument 1부터 6까지는 위 ppt처럼

Register의 정해져있는 영역에 저장하고

8번부터는 stack에 저장하게 된다

 

아무튼 그렇기 때문에 쓸데없이 함수의 argument를

많이 선언해주면 결코 performance에 좋은 영향을 줄 수 없다

왜냐하면 그럴 때마다 stack에다가

save하고 다시 roll up 시키고의 과정을 해야하기 때문이다

이런걸 가장 빠르게 하는 방법이 Register window라고 하는 방법인데

함수 호출마다 register를 stack에 저장하지 않고

register 자체를 switching하는 방법이라고 한다

 

 

 

이건 뭐...

최대한 memory movement 시키지 않으려고

짠 코드라고 한다...

 

 

 

오늘 수업의 마쥐막 내용인

Manging Local Data

 

각 function들이 내부에서 호출될 때

call frame이 어떻게 되는지 살펴보자

 

call frame은 위와같이 되지만

stack frame은 call을 부르면 부를수록

더 밑으로 무한히 내려온다고 한다

(아무튼 함수 call 너무 많이 하지말자는 뜻인듯)

 

 

 

함수를 호출할 때 stack frame이 어떻게 되는지를

설명하는 ppt이다

 

현재 실행중인 함수를 caller

그다음에 실행될 함수를 callee라고 부른다

 

현재에는 수행 중인 함수에 대한

function stack frame이

stack memory에 존재하고

%rsp와 %rbp가 해당 stack frame을 가리킨다

 

그러다가 새로운 함수인 callee를 호출하면

callee 함수에 대한 새로운 stack frame이 생성되고

현재 %rsp의 값을 %rbp로 이동한다

이렇게 되면 현재의 제어 흐름을

callee함수에게 전달하게 된 것이다

 

그런 다음 callee 함수의 수행이 끝난다면

popq를 통해 callee 함수의 stack frame을 해제한다

그런 다음 다시 caller함수의 다음 instruction 주소로 돌아가게되고

제어 흐름을 caller 함수에게 넘겨주게 된다

 

 

함수의 stack frame 구조의 예시이다

함수 call을 이렇게 하게되고

함수가 return 될 때마다 %rbp와 %rsp는

한 칸씩 올라간다

 

요롷게..

 

 

 

바로 이렇게..

 

옛날의 아키텍처에서는 rbp, rsp, return address

3개를 무조건 저장하게 했다고 한다

이게 call convention이었다고..

 

 

linux에서 동작시킬 때의 stack frame이라고 한다

stack의 가장 top에 있는 stack frame이

현재 실행중인 procedure이고 callee이다

 

caller의 stack frame은 argument랑 지역변수들을 갖고 있고

그 아래에 return address를 갖고있다

 

 

이 예시를 한 번 잘 살펴보자

incr가 받아와야하는 argument는 2개이다

그래서 v1인 15213을 넣고 뒤에는 3000이라는 constant를 넣어줬다

 

이렇게 되면 내부 stack에서는 어떻게 작동할까

우선 처음에는 rsp를 내리고

v1인 temporary 변수를 저장한다

그런다음 leaq까지 수행하며 passing 해야하는

data들을 준비시킨 다음

call incr를 호출한다

 

그럼 incr 함수로 가서 포인터 타입을 저장하고 add를 해준다

그러고 add 시킨 값을 다시 포인터에 move 해준다

이미 위에서 %rax에 setting 했기 때문에

따로 추가적인 작업을 수행하지 않고 바로 return 해준다

 

그런다음 다시 call_incr로 돌아오는데

8번째 라인인 addq $16, %rsp는

v1같은 temporary variable을 stack에서 다시 없애고

%rsp를 위로 roll up 시켜주는 부분이다

 

 

 

이게 linux에서 caller가 담당해야하는 영역이라고 한다

 

 

 

이건 callee가 담당해야하는 영역..

 

 

사실 이번 수업은 몸이 안좋아서 수업에 제대로 집중을 못했다 ㅎ..

그래서 정리하면서도 도대체 뭘 정리하고있는건지

잘 이해가 안갔던 ㅎㅎ..

 

그래도 크게 요약하자면

user program 내에서 function call에 대한 내용이고

function call이 들어오면

지금까지 수행중이던 내용들을 stack에 저장하고

호출하는 함수 실행이 끝났을 때

바로 다음에 실행해야하는 instruction의 주소를 return address라고 하고

stack frame의 가장 마지막 영역에 넣어준다

 

그럼 call된 함수를 수행하고

마지막 return address 부분을 보고

다음 instruction을 수행할 수 있게 되는 것이다

 

그 과정에서 stack의 popq, pushq가

굉장히 중요한 procedure로 작동하고

이런 과정에서 호출하는 함수를 caller, 호출되는 함수를 callee라 부르고

각각 담당해야하는 영역이 정해져있다

 

 

 

다음 시간에는 linux 운영체제 내에서

exception call에 대한 내용을 알아본다고 한다 

 

그럼 이번 수업은 여기까지 -!