CSAPP 독서 내용 정리 3-6 ~ 3-7

이형준·2023년 5월 3일
0

CSAPP

목록 보기
5/10

조건 코드 사용하기


어제 공부했던 비교 및 시험 인스트럭션의 친구인 Set 인스트럭션이다. Set 인스트럭션은 하나의 오퍼랜드를 목적 오퍼랜드로 가지고, 조건 코드 레지스터의 특정 비트를 검사하여 조건을 판단하고, 목적 오퍼랜드나 목적 오퍼랜드의 메모리 위치에 참 - 1 거짓 - 0 을 설정한다.

얘네들은 1바이트 크기의 값을 설정하고, 단순히 조건 코드 레지스터를 읽어내 조건을 판단하는 녀석이므로, 원하는 조건에 따라 조건 코드 레지스터들을 변경해주는 인스트럭션들과 같이 쓰인다.

포인트는 Synonym(동의어)의 존재인데, setg(더 큰 경우에 1을 저장)과 setnle(작거나 동일하지 않으면 1을 저장)처럼 같은 동작을 하는 인스트럭션들이 존재한다. 접미어를 잘 살펴본다면 이해하는 건 어렵지 않을듯?

점프(Jump) 인스트럭션


일반적인 실행의 경우, 인스트럭션들은 나열된 순서에 따라 순차적으로 실행된다. 점프 인스트럭션은 이러한 흐름을 특정 위치로 이동시켜주는 인스트럭션이다. 이는 무조건 분기와 조건 분기로 나뉜다. 간단히 무조건 분기는 정말 무조건! 분기가 수행되는 인스트럭션이고, 조건 분기는 조건 코드 레지스터를 검사하여 분기를 결정하는 인스트럭션이다.

점프 방법도 직접 점프와 간접 점프로 나뉜다. 점프 목적지가 인스트럭션의 일부로 인코딩되는 경우에는 직접 접프를 사용하고, 점프 대상을 레지스터나 메모리 위치로부터 읽어들여야 하는 경우에는 간접 점프를 사용한다. 예를 들어 보면,

jmp .L1

와 같은 인스트럭션은 프로그램 내의 목표 레이블 L1으로 이동한다. 이러한 것이 직접 점프!

jmp *%rax

와 같은 인스트럭션들이 간접 점프이다. 간접 점프는 '*' 와 목적 오퍼랜드를 합쳐서 작성한다. 위의 예시는 %rax의 값을 점프 목적지로 사용한다.

jmp *(%rax)

이런 인스트럭션은 %rax에 저장된 값을 읽기 주소로 사용하여 메모리에서 점프 목적지를 읽어들이겠지? 😄

점프 인스트럭션 인코딩

이 부분은 살짝 난해해서 이해하기 힘들었는데.. 중요한 포인트는 점프 인스트럭의 대상이 어떻게 인코딩되는지를 알면 좋다는 것인듯? 가장 일반적인 방법으로 PC 상대적 (PC relative)방법이 있는데, 대상 인스트럭션과 점프 인스트럭션 바로 다음에 오는 인스트럭션 주소와의 차이를 인코딩한다.

위의 사진을 살펴보자. 위는 어셈블리어고, 아래는 그 어셈블리어의 역어셈블 버전이다.

역어셈블 버전의 두번째 줄(점프)을 살펴보면, 왼쪽 기계어 코드의 두번째 바이트가 03(0x03)인것을 확인할 수 있다. 이것이 아까 말한 PC 상대적 방법인데, 이것을 다음 인스트럭션 주소(맨 왼쪽)인 0x05에 더하면 목적지 주소인 0x08을 얻을 수 있으며, 이것이 바로 4번째 줄에 있는 인스트럭션의 주소(점프로 가야할 곳)이다.

두 번째 점프 부분인 다섯 번째 줄도 확인해보자. 두번째 바이트는 0xf8 이는 2의 보수 표현 방식으로 십진수 -8, 이를 다음 인스트럭션 주소인 0xd(십진수 13)에 더하면 5. 다섯 번째 줄이 바로 점프의 목적지이다.

이하 제어문 파트는 양도 방대하고, 무리 없이 이해할 수 있었기에 따로 포스팅하지 않겠다. 궁금한 분들은 어셈블리 제어문 검색하면 좋은 자료들이 많을듯! 👍

프로시저

프로시저는 일련의 작업 단계를 수행하는 코드 블록으로, 프로그래밍에서의 함수랑 유사한 면이 많다. 프로시저 P가 Q를 호출하고, Q가 실행한 후에 다시 P로 리턴한다고 가정해보자. 이 때 필요한 기능은 무엇이 있을까?

  • 제어권 전달: 프로그램 카운터(PC)는 진입할 때 Q에 대한 코드의 시작주소로 설정되고, 리턴할 때는 P에서 Q를 호출하는 인스트럭션 뒤로 설정되어야 할 것이다.

  • 데이터 전달: P는 하나 이상의 매개변수를 Q에 제공할 수 있어야 하며, Q는 다시 Q로 하나 이상의 값을 리턴할 수 있어야 한다.

  • 메모리 할당과 반납: Q는 시작할 때 지역변수들을 위한 공간을 할당할 수 있고, 리턴할 때 이 저장소를 반납할 수 있다.

이러한 프로시저 호출을 구현하기 위해, 연관된 오버헤드 발생을 최소화하기 위해 갖은 노력이 동반되었고, 결과적으로 각 프로시저가 요구하는 메커니즘만을 최소한으로 구현하는, 최소주의자? 이득충? 같은 방식들을 따르게 되었다. 그러한 내용들을 하나하나 알아볼 차례다.

런타임 스택

프로시저 호출 동작방식은 스택 자료구조의 후입선출 방식을 따른다. 이전에 공부한 것 처럼 x86-64의 스택은 작은 주소 방향으로 성장하며, 스택 포인터는 스택의 최상위 원소를 가리킨다.

사진은 일반적인 스택 프레임 구조를 나타낸다. x86-64 프로시저가 레지스터에 저장할 수 있는 개수 이상의 저장공간을 필요로 할 때는 공간을 스택에 할당하는데, 이 스택 영역을 프로시저의 스택 프레임이라고 부른다.

주목할 점은 프로시저 P가 Q를 호출하면서 Return address를 스택에 push해서 Q가 리턴할 때 P에서 프로그램이 재시작해야 하는 위치를 가리켜 준다는 것. 프로시저 Q가 호출되면 스택 경계를 확장해서 자신을 위한 공간을 할당한다. 그리고 이 공간 내에서 레지스터 값들을 저장하고, 지역변수를 위한 공간을 할당하며, 자신이 호출하는 프로시저들을 위한 인자들을 설정할 수 있다. 쉽게 말해 본인이 활동할 공간을 만들어서 거기서 활개친다는 느낌?

신기한 점은 많은 함수들은 스택 프레임을 요청하지도 않는다는 것이다. 대부분 레지스터 선에서 처리할 수 있다고 하네~ 일례로 지금까지의 예제 함수들은 모두 스택 프레임을 필요로 하지 않는 것들이었다고 한다.

제어의 이동

제어를 함수 P에서 Q로 넘긴다는 것은 단순히 프로그램 카운터를 Q를 위한 코드 시작주소로 설정하는 것이면 된다. 그렇지만, 나중에 Q가 동작을 마치고 리턴해야 할 때가 오면 프로세서는 P의 실행을 다시 실행해야 하는 코드 위치의 일부 기록을 가지고 있어야 한다. 이를 담당하는 것이 call 인스트럭션! 한번 살펴보자.

call 인스트럭션은 호출된 프로시저가 시작하는 주소를 목적지로 갖는다. jump 인스트럭션과 비슷하게 직접 호출할수도, *를 붙여 간접 호출할 수도 있다.

ret은 프로시저 호출이 끝날 때 사용되는 인스트럭션으로, 프로시저가 호출될 때 스택에 저장해 놓았던 복귀 주소를 읽어들여, 스택 프레임을 해제하고 해당 주소로 복귀한다.

사진을 보면 더 명확하게 이해가 가능한데, 이와 같이 리턴 주소를 스택에 푸시하는 간단한 방법을 통해 함수가 나중에 프로그램의 적절한 위치로 리턴이 가능하게 되는 걸 알 수 있다.

데이터 전송

지금까지 다양한 어셈블리어 예제를 통해 값들이 rdi, %rsi 등으로 %rax로 리턴되는 등, 데이터가 어떻게 이동하는 지 보아왔다. 그렇다면 왜 하필 %rdi고, %rax로 리턴되는가? 이러한 관습에 대해 더 자세하게 공부해보자.

x86-64에서는 최대 여섯 개의 정수형(즉, 정수와 포인터) 인자가 레지스터로 전달될 수 있다. 이 레지스터들은 전달되는 데이터 형의 길이에 따라 레지스터 이름을 이용해서 정해진 순서로 이용된다.

  • 함수 인자 전달을 위한 레지스터들 (1~6)

그렇다면 함수가 여섯 개 이상의 정수형 인자를 가진다면? 다른 인자들은 스택으로 전달된다. 인자 1~6은 적절한 레지스터에 복사하고, 인자 7에서 n까지는 인자 7을 스택 탑에 넣는 방법으로 저장한다.

추가 공부: 스택 프레임 할당에 대해❓

  • 함수가 호출되면 스택 프레임을 할당합니다.

스택 프레임은 해당 함수의 지역 변수, 함수 인자, 반환 주소 등을 저장하기 위해 할당됩니다.
할당된 스택 프레임 크기는 함수에서 사용되는 지역 변수 및 함수 인자의 크기에 따라 결정됩니다.

이 때, 함수에서 사용되는 지역 변수와 함수 인자는 스택 프레임의 하단에서부터 상단으로 쌓입니다.
스택 프레임이 할당되면, 해당 함수에서 사용되는 레지스터들의 값을 스택에 저장해야 합니다.

이렇게 하면, 스택 프레임이 손상되지 않고 함수 호출이 종료된 후에도 레지스터 값이 보존됩니다.
함수가 반환되면, 스택 프레임은 제거됩니다.

이 때, 스택 프레임의 상단에는 반환 주소가 저장되어 있습니다.
반환 주소는 호출한 함수로 되돌아갈 때 사용됩니다.

함수 호출 중 스택 프레임이 부족해지면 스택 오버플로우가 발생합니다.
이는 스택에 할당된 메모리 공간을 넘어서는 데이터가 저장될 때 발생합니다.
스택 오버플로우는 프로그램의 보안 취약점으로 악용될 수 있으므로 주의해야 합니다.
반대로, 스택에 할당된 공간이 너무 많은 경우에는 스택 초과(스택 오버플로우와 반대 개념)가 발생할 수 있습니다.

profile
저의 미약한 재능이 세상을 바꿀 수 있을 거라 믿습니다.

0개의 댓글