240128_정보 접근, 프로시저, 배열 할당 및 접근

추성결·2024년 1월 28일
1

참조: https://hon99oo.github.io/csapp/csapp_05/

정보 접근하기

  • 인스트럭션들은 16개의 레지스터 하위 바이트들에 저장된 다양한 크기의 데이터 연산이 가능하다.
  • 오퍼랜드(operland)는 연산을 수행할 소스값과 그 결과를 저장할 목적지의 위치를 명시한다.
  • 오퍼랜드의 종류는 세가지 타입으로 구분한다.
    1.Immediate(상수): ATT형식의 어셈블리 코드에서 상수는 $기호 뒤에 C표준 서식을 사용하는 정수이다.
    2. Register: 레지스터의 내 각각 16개의 64,32,16,8비트 레지스터들의 하위 일부분인 8,4,2,1 바이트 중 하나의 레지스터를 가리킨다.
    3. 메모리에서 가져온 값: 유효 주소라고 부르는 계산된 주소에 의해 메모리 위치에 접근한다.

데이터 이동 인스트럭션

  • mov클래스: 이 인스트럭션들은 소스 위치에서 데이터를 목적지 위치로 어떤 변환도 하지 않고 복사하며, 4개의 인스트럭션(movb(1byte), movw(2byte), movl(4byte), movq(8byte))으로 구성된다.
  • X86-64는 데이터 이동 인스트럭션에서 두개의 오퍼랜드가 메모리 위치에 올 수 없도록 제한한다.

데이터 이동 예제

  • C언어에서 "포인터"라고 부르는 것이 어셈블리어에서는 단순히 주소라고 한다.
  • 지역 변수들은 메모리에 저장되기 보다는 종종 레지스터에 저장된다.

스택 데이터의 저장과 추출

  • 스택은 프로시저의 호출을 처리하는데 중요한 역할을 한다.
  • 쿼드워드 값을 스택에 추가하려면, 먼저 스택 포인터를 8 감소 시키고, 그 값을 스택 주소의 새로운 탑(stack의 top)에 기록하는 것으로 구현한다.

프로시저

  • 프로시저 호출은 소프트웨어에서의 주요 추상화이다.
  • 잘 설계된 소프트웨어는 무슨 값이 계산되고, 이 프로시저가 프로그램 상태에 무슨 효과를 갖는지에 대한 명쾌하고 간결한 인터페이스 정의를 제공하는 한편, 일부 동작의 구체적인 구현은 감춰주는 방식으로 프로시저를 추상화 매커니즘으로 이용한다.(ex. 함수, 메소드, 핸들러 등)

이후 내용에서 프로시저 P가 프로시저 Q를 호출하고, Q 실행 후 다시 P로 리턴된다고 가정한다

  • 위의 동작들은 하나 이상의 메커니즘이 연관된다.
    1. 제어권 전달: 프로시저를 시작하고 return point로 돌아오는 것을 제어한다.
    2. 데이터 전달: 인자를 전달하고 Value를 리턴한다.
    3. 메모리 할당과 반납: 프로시저 실행 중, 메모리를 할당하고, 리턴때 메모리를 반납한다.

런타임 스택

  • C언어와 다른 대부분의 언어에서의 프로시저 호출 방식은 stack 자료구조를 사용한다.
  • 프로시저 P가 프로시저 Q를 호출 시 리턴 주소(return address)를 스택에 푸시하는데, 이는 Q가 리턴할 때 P에서 프로그램이 실행을 재시작해야하는 위치를 가리킨다. 또한, Q를 실행하고 P는 일시적으로 정지된다.

제어의 이동

  • 제어를 함수 P에서 함수 Q로 전달하는 것은 단순히 PC를 Q를 위한 코드의 시작 주소로 설정하는 것과 관련된다. 나중에 Q가 리턴해야 할 때가 오면 '프로세서'는 P의 실행을 다시 시작해야하는 코드 위치 정보를 갖고 있어야한다.
  • X86-64에서 해당 정보를 인스트럭션 'call'이 해당 작업을 담당하는데, 리턴 주소를 푸시하고, PC를 Q의 시작으로 설정한다.

데이터 전송

  • x86-64에서는 최대 6개의 정수형 인자(정수와 포인터)가 레지스터로 전달될 수 있다.
  • 프로시저 P가 n개(n>6)의 정수형 인자를 가지면서 Q를 호출한다 가정할 때, P에 대한 코드는 인자 7에서 n까지를 위한 충분한 크기의 저장공간을 스택 프레임에 할당해야한다.
  • 프로시저 Q는 레지스터와 스택을 통해 자신의 인자들에 접근할 수 있다.
  • 만일 Q가 여섯개가 넘는 인자를 갖는 어떤 함수를 호출하려면, 자신의 스택 프레임에 "Argument build area"라고 이름 붙힌 영역으로 이들을 위한 공간을 할당할 수 있다.

스택에서의 지역 저장공간

  • 지역 데이터가 메모리에 저장되어야하는 경우가 있는데, 이는 아래와 같은 공통 경우를 포함한다.
    지역 데이터 모두를 저장하기에는 레지스터의 수가 부족하다.
    지역 변수에 연산자 '&'가 사용되었으며, 이 변수의 주소를 생성할 수 있어야한다.
    일부 지역 변수들이 배열 또는 구조체여서 배열이나 구조체 참조로 접근되어야한다.
  • 프로시저는 스택 포인터를 감소시켜서 스택 프레임에 공간을 할당한다. 이렇게 하면 "Local variables"로 명명된 스택 프레임의 일부분이 생성된다.

레지스터를 이용하는 지역 저장소

  • 하나의 프로시저(호출자)가 다른 프로시저(피호출자)를 호출할 때, 피호출자는 호출자가 나중에 사용할 계획인 일부 레지스터 값을 덮어쓰지 않는다.
  • 즉 Q가 P로 리턴할 때, Q가 호출되었을 때의 값들과 동일하도록 보장할 수 있게 이 레지스터들의 값을 보존해야 하는데, 프로시저 Q는 이 값을 전혀 변경하지 않거나 원래의 값을 스택에 푸시해놓고(이 때, "Saved registers로 이름 붙힌 스택 프레임의 일부분을 생성한다.) 이 값을 변경하며, 리턴하기 전에 스택에서 이전 값을 pop해오는 방식으로 레지스터를 보존한다.

배열의 할당과 접근

이후 내용에서 자료형 T와 정수형 상수 N에 대해서 아래와 같이 선언한다고 예를 들고 진행한다.

T A[N];

// 시작 위치 는 XA로 명명한다.
  • C에서는 배열 원소들에 대한 포인터를 만들고, 이들 포인터 간에 연산을 할 수 있다.
    L*N 바이트의 연속적인 공간 메모리에 할당한다.(L은 자료형 T의 크기를 말한다.)
    새로운 식별자 A를 통해서 배열이 시작하는 위치의 포인터로 사용한다. 이 포인터 값은 XA다.
    배열의 각 원소는 0~(N-1) 사이의 정수형 인덱스를 사용해 접근할 수 있다. 배열의 원소 i의 값은 주소 (XA + L*i)에 저장한다.

포인터 연산

  • C는 포인터 간에 연산을 허용하며, 계산된 값은 포인터가 참조하게 되는 자료형의 크기에 따라 그 값이 확대된다.
    => 만일 p가 자료형 T의 데이터에 대한 포인터이고, p의 값을 Xp라 하면, 수식 p+i는 (Xp + L*i)가 된다.

  • 단항 연산자(unary) '&'와 ''는 포인터의 생성과 역참조를 수행한다.
    => 어떤 객체를 나타내는 식 Expr에 대해 &Expr는 그 객체의 주소를 주는 포인터이다.
    => 주소를 나타내는 식 AExpr에 대해
    AExpr는 그 주소에 위치한 값을 준다. 따라서 식 Expr와 *&Expr는 동일하다.

다중 배열

  • 배열 할당과 참조에 관한 일반적인 원칙들은 이중 배열을 생성할 때도 사용된다.
int A[5][3];
  • 배열 A는 다섯개의 배열을 우너소로 가지며, 이들 각각은 세 개의 정수를 저장하기 위해 12바이트를 필요로 한다. 배열의 총 크기는 453 = 60바이트이다.

  • 배열의 원소들은 메모리에 "행우선(row major)"순서로 저장되는데, 이는 A[0] 모든 행 원소들 다음에 1행(A[1])의 원소가 따라오는 방식으로 저장되는 것을 의미한다.

  • T D[R][C]; 에 대해 메모리 주소는 (&D[i][j] = XD + L(C*i + j))로 계산된다.

고정 크기의 배열

  • C 컴파일러는 고정 크기의 다차원 배열을 위한 코드에 대해 다양한 최적화를 수행할 수 있다.

가변 크기의 배열

  • 역사적으로 C에서는 컴파일 시에 그 크기가 결정될 수 있는 다차원 배열만을 지원해왔다.
  • 가변크기 배열을 원하면, 배열들을 위한 저장 공간을 'malloc' 이나 'calloc' 같은 함수를 사용해서 할당하거나 다차원 배열을 1차원 배열들고 행우선 인덱싱을 사용해서 명시적으로 변환한다.

1개의 댓글

comment-user-thumbnail
2024년 2월 1일

아니.. 공부 디게 열심히 하셨네요
그림 퍼갑니다..

답글 달기