CSAPP 독서 내용 정리 3-1 ~ 3-4

이형준·2023년 5월 2일
0

CSAPP

목록 보기
3/10

프로그램의 인코딩📜

p1.c 와 p2.c란 파일 두 개를 유닉스 커맨드라인을 통해 컴파일해보자.

linux> gcc -Og -o p p1.c p2.c

gcc: 컴파일러를 의미. gcc는 리눅스의 기본 컴파일러이다. 간단히 cc로도 호출 가능

-Og: 컴파일 동작의 최적화 수준을 명령하는 부분. 최적화 수준을 올리게 되면 최종 프로그램은 빠르게 동작하지만, 컴파일이 오래 걸리고 디버깅이 어려워진다. 또한 만들어진 코드가 너무 많이 변경되어서 본래의 코드와 생성된 기계어 코드 간의 관계를 이해하기 어려워지기도.

-o: gcc컴파일러가 기본으로 지정하는 a.out이 아닌 다른 이름으로 바이너리 파일을 생성할 수 있게 해주는 명령어. 위의 경우엔 p라는 이름으로 파일이 생성된다.

기계수준 코드

ISA(Instruction Set Architecture): 명령어 집합 구조라고도 보르는 이 친구는 컴퓨터 시스템의 프로세서와 하드웨어 간의 인터페이스를 정의한다. 즉, ISA는 프로세서가 이해하고 수행할 수 있는 명령어 집합을 정의하고, 명령어의 형식과 동작 방식, 레지스터, 메모리 등의 하드웨어 구성 요소와의 상호작용 방식을 명세한다.

컴퓨터 시스템은 추상화 모델을 통해 복잡한 세부 구현내용을 감추는데, 프로세서도 예외는 아니다. 프로세서의 상태는 C 프로그래머에게 일반적으로 감추어져 있다. 예를 들면,

  • 프로그램 카운터(PC or %rip in x86-64)는 실행할 다음 인스트럭션의 메모리 주소를 가리킨다.

  • 정수 레지스터 파일은 64비트 값을 저장하기 위한 16개의 이름을 붙인 위치를 갖는다. 이들 레지스터는 주소(포인터 in C)나 정수 데이터를 저장할 수 있다. 일부 레지스터는 프로그램의 중요한 상태를 추적하는 데 사용될 수 있으며, 다른 레지스터들은 함수의 리턴 값뿐만 아니라 프로시저의 지역변수와 인자 같은 임시 값을 저장하는데 사용하기도 한다. 당연하게도 읽는 속도는 무진장 빠르겠지? 😁

  • 조건코드 레지스터들은 가장 최근에 실행한 산술 또는 논리 인스트럭션에 관한 상태 정보를 저장한다. 이들은 if나 while문을 구현할 때 필요한 제어나 조건에 따른 데이터 흐름의 변경을 구현하기 위해 사용한다.

  • 벡터 레지스터들의 집합은 하나 이상의 정수나 부동소수점 값들을 각각 저장할 수 있다.

    하나의 기계어 인스트럭션은 매우 기초적인 동작만을 수행한다. 예를 들어 레지스터들에 저장된 두 수를 더하고, 메모리와 레지스터 간의 데이터 교환, 새로운 인스트럭션 주소로 조건에 따라 분기하는 등.

코드 예제

기계어 코드 파일의 내용을 조사하려면, 역어셈블러라고 하는 프로그램이 매우 중요해진다. 이 프로그램들은 기계어 코드로부터 어셈블리어 코드와 유사한 형태를 생성한다.

linux> objdump -d mstore.o

결과는 다음과 같다.

알아둘 점은, 역어셈블러는 다른 입력 없이 기계어 파일의 바이트 순서에만 전적으로 의존해서 어셈블리어 코드를 생성해 낸다는 것과, GCC가 생성한 어셈블리어 코드와는 다르게 'q'와 같은 접미어들이 생략된 것. 이러한 접미어는 크기를 나타내는 것으로, 대부분의 경우 이러한 접미어들은 생략이 가능하다.

다른 예시를 살펴보자. 이번엔 main 안의 mulstore을 역어셈블!

우선 주목할 점은 첫째 줄의 주소가 달라졌다는 것callq 인스트럭션이 함수 mult2를 호출할 때 사용해야 하는 주소를 채웠다는 점이다. 이는 모두 링커가 한 일인데, 링커의 임무 중 하나는 이들 함수들을 위한 실행 코드의 위치들과 함수 호출을 일치시키는 것이다.

또한 retq이후에도 의미없는 인스트럭션들이 자리를 차지하고 있는 모습도 확인할 수 있는데(8~9줄), 이는 어셈블리 코드를 만드는 과정에서의 최적화 기법 중 하나로 "nop insertion" or "padding" 이라고 한다. 이렇게 함으로써 코드가 특정한 크기를 유지할 수 있고, 이로 인해 CPU의 캐시 성능을 최적화하거나 코드 블록의 경계를 일정하게 유지하여 코드 해석을 용이하게 해주는 장점이 있다.

데이터의 형식

  • GCC가 생성한 대부분의 어셈블리 코드 인스트럭션들은 오퍼랜드의 크기를 나타내는 단일문자 접미어를 가지고 있다. movb(바이트 이동), movw(워드 이동), movl(더블워드 이동), movq(쿼드워드 이동). 여기서 l이 왜 더블워드를 나타내는 접미사인지 헷갈렸는데, 32비트 양이 long word로 간주되기 때문이란다.

정보 접근하기

상술했듯 x86-64 주처리장치 CPU는 64비트 값을 저장할 수 있는 16개의 범용 레지스터를 보유하고 있다. 원조인 8086에서 레지스터들은 그림의 %ax ~ %sp까지 나타낸 것처럼 16비트 레지스터를 가지고 있었으나, 32비트로 확장되며 %eax ~ %esp까지 이름을 붙였으며, 이후 64비트로 확장되며 %rax ~ %rsp까지 확대되었다. 64비트 시스템에선 기존의 8개의 레지스터에 더해 r로 시작하는 8개의 레지스터가 추가되었다.

이 중 가장 특이한 녀석은 %rsp로, 런타임 스택의 끝 부분을 가리키기 위해 사용된다. 이 녀석은 일반적인 정수 레지스터들과 몇 가지 중요한 차이점이 있다. 추후 스택에 관련되서 또 등장하는 녀석이라 나중에 더 자세히 알아보자.

오퍼랜드 식별자

대부분의 인스트럭션은 하나 이상의 오퍼랜드를 가진다. 오퍼랜드는 연산을 수행할 소스값과 그 결과를 저장할 목적지 위치를 명시한다. 소스 값은 상수 or 레지스터 or 메모리, 결과 값은 레지스터 or 메모리에 저장된다. 상수 값은 소스에만 들어갈 수 있음에 유의.

이 오퍼랜드(Operand)라는 개념이 살짝 헷갈려서 검색을 좀 해봤다. 오퍼랜드란 컴퓨터 프로그램에서 연산자(Operator)가 작동하기 위해 필요한 데이터나 값을 말한다.

예를 들어, "add %rax, %rbx"라는 인스트럭션에서는 "add"가 연산자(Operator)이고, "%rax"와 "%rbx"가 오퍼랜드이다.

다른 예로, "movl $10, %eax"라는 인스트럭션에서는 "movl"이 연산자(Operator)이고, "$10"과 "%eax"가 오퍼랜드이다.

여기부턴 독서하며 메모한 것들~




스택 데이터의 저장과 추출

위의 두 데이터 이동 연산은 프로그램 스택에 데이터를 저장(Push)하거나 추출(Pop)하기 위해 사용한다. 스택은 프로시저 호출을 처리하는 데 중요한 역할을 한다.

그림의 스택 Top원소가 모든 스택 원소 중에서 가장 낮은 주소를 갖는 형태로, 스택은 아래 방향으로 성장한다 대부분의 경우 이를 뒤집어 그려 설명하는 경우가 많은데, 스택의 Top이 가장 낮은 주소를 나타낸다는 것을 알아두면 좋을듯 😄

쿼드워드 값을 스택에 추가하거나 뺀다고 가정해보자. 값을 Push하는 경우는 먼저 스택 포인터를 8 감소시키고(주소값을 내린다), 그 값을 새로운 Top에 기록하는 것으로 구현된다. 반대로 값을 Pop하는 경우는 스택 Top 위치에서 읽기 작업 후에 스택 포인터를 8 증가시킨다(주소값을 올린다)

그림의 가장 오른쪽을 보자. pop 이후 스택에 더는 포함되지 않지만, 값 0x123은 다른 값이 덮어써질 때 까지 메모리 주소 0x100에 여전히 남아 있다. 하지만 누가 뭐래도 %rsp가 가리키는 곳만이 스택 탑이고, 이보다 아래에 있는 값들은 모두 무효인 값인 것이 포인트!

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

0개의 댓글

관련 채용 정보