jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. 3.10 제어와 데이터를 결합하기
    1. 3.10.1 포인터 이해하기
      1. 모든 포인터는 관련된 타입을 가진다
      2. 모든 포인터는 값을 가진다
      3. 포인터는 & 연산자로 생성된다
      4. 포인터는 * 연산자로 역참조한다
      5. 배열과 포인터는 밀접한 관계가 있다
      6. 포인터 타입 변환은 타입만 바꾸고 값은 바꾸지 않는다
      7. 포인터는 함수도 가리킬 수 있다
    2. 3.10.2 현실 세계에서의 활용: gdb 디버거 사용하기
    3. 3.10.3 범위를 벗어난 메모리 참조와 버퍼 오버플로
      1. 버퍼 오버플로
    4. 3.10.4 버퍼 오버플로 공격 방어
      1. 스택 랜덤화
      2. 스택 손상 감지
      3. 실행 가능 코드 영역 제한
    5. 3.10.5 가변 크기 스택 프레임 지원
      1. 요점
      2. 정리
      3. 스택 프레임 구조 예시 (vframe 함수)

csapp 3.10 이다


3.11 이 3단원 끝이니 얼른 끝내자


ㅅㅂ 하기 졸라게 싫다 진짜



3.10 제어와 데이터를 결합하기

데이터와 제어가 서로 상호작용하는 것에 대해 설명한단다

포인터와, gdb, 버퍼 오버플로, 스택에 필요한 저장 공간 변경
을 다룬다


3.10.1 포인터 이해하기

포인터에 대한 설명이 나온다

모든 포인터는 관련된 타입을 가진다

int *ip;
char **cpp;
  • 예시 코드를 보면 ipint 타입 객체를 가리키는 포인터다

  • cppchar 타입 객체를 가리키는 포인터를 가라키는 포인터다

  • 일반적으로, 객체의 타입이 T라면 포인터 타입은 *T

  • 특별한 타입 void *는 범용 포인터다

  • 포인터 타입 정보는 기계어에는 없고 C언어에서 제공하는 추상 개념이다


모든 포인터는 값을 가진다

  • 이 값은 해당 타입의 객체가 저장된 주소

  • 단, NULL(0)은 아무것도 가라키지 않다는 뜻이다


포인터는 & 연산자로 생성된다

  • & → 변수나 구조체·배열 요소(lvalue)의 주소를 가져옴

  • 기계어에서는 주로 leaq 명령어로 주소 계산


포인터는 * 연산자로 역참조한다

  • * → 해당 주소에 있는 값을 가져오거나 저장

  • 기계어에서는 메모리 읽기/쓰기 명령으로 구현


배열과 포인터는 밀접한 관계가 있다

  • 배열 이름은 포인터처럼 참조 가능(단, 값 변경 불가)

  • a[3]*(a + 3) → 완전히 동일한 의미

  • 포인터 산술(pointer arithmetic)은 타입 크기만큼 주소를 이동


포인터 타입 변환은 타입만 바꾸고 값은 바꾸지 않는다

  • 캐스팅1주소 값은 그대로지만, 포인터 산술 시 단위 크기가 바뀜
(char *)p + 7   // p + 7
(int  *)p + 7   // p + (7 * sizeof(int))
  • 캐스팅 연산자는 덧셈보다 우선순위 높음


포인터는 함수도 가리킬 수 있다

  • 함수의 시작 주소를 저장하여 다른 곳에서 호출 가능
int fun(int x, int *p);
int (*fp)(int, int *);
fp = fun;
int result = fp(3, &y);
  • 함수 포인터 값 = 해당 함수 첫 번째 명령어 주소



3.10.2 현실 세계에서의 활용: gdb 디버거 사용하기

gdb라는 디버거 소개한다

솔직히 gdb같은 거 요즘은 안쓰니까 걍 넘어가겠다

뭐 나중에 쓰면 그때에 다시 제대로 정리하겠다



3.10.3 범위를 벗어난 메모리 참조와 버퍼 오버플로

C언어는 배열 참조시 경계 검사 안한다

뭐 어쩌라고라 생각 할 수 있지만 꽤 중요하다

범위를 벗어난 쓰기가 발생해 데이터 손상 가능하고 이로 인해 오류 발생하니 말이다


버퍼 오버플로

대표적인게 버퍼 오버플로다

배열에 할당된 공간보다 문자열 길이가 크면 발생한다

이러할 경우 다른 메모리 공간을 침범하기에

악용 가능하다

초과하는 길이의 문자열에 익스플로잇 코드를 넣는 등 말이다



3.10.4 버퍼 오버플로 공격 방어

ㅈㄴ 흔한 공격이니 방어하는 법 정돈 알아야한다

스택 랜덤화

공격하려면 코드와 코드의 주소 포인터 다 알아야 한다

그러니 이를 무력화시키기 위해 실행시마다 스택의 위치를 다르게 하는 것이다


스택 손상 감지

두 번째는 스택 손상 여부를 감지하는 것이다

카나리아 값이라는 특별한 보호값을 끼워넣어

이것이 변했는지 체크하며 변했을 경우 즉시 프로그램 종료한다


실행 가능 코드 영역 제한

마지막 방어선은 실행 가능 코드 삽입 능력 자체를 제거하는 것이다

스택, 힙, 전역 데이터 영역 등을 읽기/쓰기는 가능하지만 실행 불가로 설정하는 방법이다



3.10.5 가변 크기 스택 프레임 지원

요점

  • 가변 크기 지역 저장소(예: 실행 시 크기 결정되는 배열, alloca 호출)가 있을 때
    컴파일 시 스택 크기 예측 불가

  • 프레임 포인터(%rbp) 사용해 고정 위치 참조
    → 고정 크기 지역 변수와 가변 크기 영역을 안정적으로 접근 가능

  • %rbpcallee-saved → 함수 시작 시 저장, 종료 시 복원

  • leave 명령 → 스택 프레임 전체 해제 (movq %rbp, %rsp + popq %rbp와 동일)

  • x86-64에서는 필요한 경우에만 프레임 포인터 사용 (옛 IA32는 기본 사용)



정리

1. 문제 상황

  • 지역 배열 크기가 호출마다 달라지는 경우
  • 지역 변수와 가변 배열 모두 접근해야 함
  • 스택 프레임 크기 컴파일 시 결정 불가

2. 해결 방법

  • %rbp를 프레임 포인터로 설정
  • 고정 변수: %rbp 기준 고정 오프셋 참조
  • 가변 배열: %rbp 아래쪽에 필요한 만큼 동적 공간 확보

3. 실행 흐름

  1. %rbp 저장 (pushq %rbp)
  2. %rbp%rsp
  3. 고정 변수 공간 할당
  4. 배열 크기에 맞춰 추가 공간 할당
  5. 작업 수행 (배열/변수 접근)
  6. leave로 프레임 해제
  7. ret

스택 프레임 구조 예시 (vframe 함수)

높은 주소
┌─────────────────────┐
│ Return address      │
├─────────────────────┤
│ Saved %rbp          │  ← %rbp
├─────────────────────┤
│ i (8 bytes)         │
├─────────────────────┤
│ (Unused) (8 bytes)  │
├─────────────────────┤
│ p[0] ... p[n-1]     │  (8n bytes)
└─────────────────────┘  ← %rsp
낮은 주소






  1. 값의 타입을 강제로 바꾸는 것