자료형 사이즈 확인하기
#include <stdio.h> int main() { printf("Size of char: %zu bytes\n", sizeof(char)); printf("Size of short: %zu bytes\n", sizeof(short)); printf("Size of int: %zu bytes\n", sizeof(int)); printf("Size of long: %zu bytes\n", sizeof(long)); printf("Size of long long: %zu bytes\n", sizeof(long long)); printf("Size of float: %zu bytes\n", sizeof(float)); printf("Size of double: %zu bytes\n", sizeof(double)); printf("Size of long double: %zu bytes\n", sizeof(long double)); printf("Size of pointer: %zu bytes\n", sizeof(void*)); return 0; }
출력
Size of char: 1 bytes Size of short: 2 bytes Size of int: 4 bytes Size of long: 8 bytes Size of long long: 8 bytes Size of float: 4 bytes Size of double: 8 bytes Size of long double: 16 bytes Size of pointer: 8 bytes
문자열 오버플로우
#include <stdio.h> #include <limits.h> int main() { char value = CHAR_MAX; printf("Original value: %d\n", value); value = value + 1; printf("Value after adding 1: %d\n", value); return 0; }
출력
Original value: 127 Value after adding 1: -128
비트연산
특정 위치의 비트 값이 1인지 확인하는 함수
int is_bit_set(unsigned char value, int position) { return (value & (1 << position)) != 0; }
특정 위치가 1인 마스크 생성
(1 << position) and 연산으로 두 비트를 곱함
만약 원본 비트가 1이라면
1&1 != 0 특정 위치의 비트를 1로 설정하는 함수
unsigned char set_bit(unsigned char value, int position) { return value | (1 << position); }
특정 위치가 1인 마스크 생성
(1 << position) or 연산으로 두 비트를 곱함
마스크에 의해 하나 이상의 1이 존재 →
1 | x[0,1] = 1특정 위치의 비트를 끄는 함수
unsigned char clear_bit(unsigned char value, int position) { return value & ~(1 << position); }
특정 위치가 1인 마스크 생성
(1 << position) not 연산으로 마스크를 뒤집음
~특정 위치만 마스크가
0 이고 나머지는 1 때문에, and 연산시 원하는 위치만 0 으로 처리됨검증하기
#include <stdio.h> int main() { unsigned char value = 0b00001000; // 예: 3번째 비트만 1입니다. // 3번째 비트가 설정되어 있는지 확인 if (is_bit_set(value, 3)) { printf("3rd bit is set!\n"); } else { printf("3rd bit is not set!\n"); } // 2번째 비트를 설정 value = set_bit(value, 2); printf("Value after setting 2nd bit: %d\n", value); return 0; }
출력
3rd bit is set! Value after setting 2nd bit: 12
실습
오버플로우 예제를 언더플로우로 바꿔보자
#include <stdio.h> #include <limits.h> int main() { char value = CHAR_MIN; printf("Original value (CHAR_MIN): %d\n", value); value = value - 1; printf("Value after subtracting 1: %d\n", value); return 0; }
출력
Original value (CHAR_MIN): -128 Value after subtracting 1: 127
컴파일 과정 실습

전처리 과정 (.i 파일 확인하기)
gcc -E hello_world.c -o hello_world.i

컴파일 과정 (.s 파일 확인하기)
gcc -S hello_world.i -o hello_world.s
.file "hello_world.c" .text .section .rodata .LC0: .string "hello world" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rax movq %rax, %rdi movl $0, %eax call printf@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
어셈블 과정 (.o 파일 확인하기)
gcc -c hello_world.s -o hello_world.o

오브젝트 파일(.o)이란?
소스 코드가 컴파일된 후의 중간 결과물로, 링커에 의해 실행 가능한 바이너리나 라이브러리로 만들어지기 전의 형태입니다.
오브젝트 파일의 주요 특징
- 바이너리 형태: 기계어 코드를 포함하는 바이너리 파일입니다. 그러나 이 파일 자체로는 실행될 수 없으며, 다른 오브젝트 파일이나 라이브러리와 링크되어야 비로소 실행 가능한 바이너리가 됩니다.
- 재배치 가능: 다른 오브젝트 파일이나 라이브러리와 링크되어 완전한 프로그램을 형성합니다. 이 과정에서 링커는 각각의 변수나 함수 참조를 올바른 메모리 주소나 함수로 연결해 줍니다.
- 심볼 테이블: 파일 내에 심볼 테이블을 포함합니다. 이는 파일 내의 각 함수나 변수의 이름과 위치 정보를 나타내며, 링커가 다른 파일들과 링크 작업을 수행할 때 참조하는 중요한 정보입니다.
- 플랫폼 및 컴파일러 특정: 특정 아키텍처, 운영 체제, 컴파일러에 따라 형태가 다릅니다. 대표적인 확장자로는 UNIX/Linux 시스템에서는
.o, Windows에서는.obj를 사용합니다.
파일 분석 도구
1. readelf
- 전용 도구: ELF 파일 형식에 특화된 분석 도구로, ELF 파일의 모든 섹션과 헤더 정보를 아주 자세히 확인할 수 있습니다.
- 출력 형식: 분석 결과를 사람이 읽고 이해하기 쉬운 텍스트 형식으로 제공합니다.
- 주요 기능: ELF 파일의 헤더, 섹션 헤더, 프로그램 헤더, 심볼 테이블, 노트 섹션 등의 세부 정보를 화면에 표시합니다.

2. objdump
- 다기능 도구: ELF 파일뿐만 아니라 다른 포맷의 바이너리 파일 분석에도 범용적으로 사용할 수 있는 도구.
- 디스어셈블 (Disassemble): 가장 강력한 기능 중 하나로, 바이너리 형태의 기계어 코드를 어셈블리 언어로 변환(디스어셈블)하여 보여줍니다. 이를 통해 프로그램의 내부 동작을 직접 코드로 뜯어볼 수 있다.
- 다양한 정보 제공: 바이너리의 헤더 정보, 섹션 내용, 심볼 테이블은 물론 공유 라이브러리 의존성, 재배치 정보 등 폭넓은 정보를 확인할 수 있다.
> objdump -d hello_world.o hello_world.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf> f: 48 89 c7 mov %rax,%rdi 12: b8 00 00 00 00 mov $0x0,%eax 17: e8 00 00 00 00 call 1c <main+0x1c> 1c: b8 00 00 00 00 mov $0x0,%eax 21: 5d pop %rbp 22: c3 ret
바이너리 파일 정보 분석 결과
file hello_world.o
출력

- ELF (Executable and Linkable Format)
- 유닉스 및 유닉스 기반 운영체제에서 사용되는 표준 바이너리 파일 포맷.
- 주로 실행 파일, 오브젝트 파일, 공유 라이브러리 등에 사용된다.
- 64-bit
- 해당 파일이 64비트 아키텍처 시스템 환경에 맞춰 컴파일되었음을 의미.
- LSB (Least Significant Byte)
- 리틀 엔디안(Little Endian) 방식의 바이트 정렬 순서를 사용.
- relocatable
- 재배치가 가능한 오브젝트 파일임을 나타냄.
- 즉, 링커(Linker)를 통해 다른 오브젝트 파일이나 공유 라이브러리와 결합(Link)될 수 있는 상태이다.
- x86-64
- 이 파일이 인텔의 x86-64 (또는 AMD64) CPU 아키텍처에서 동작하도록 컴파일되었음을 알려준다.
- version 1 (SYSV)
- 사용된 ELF 포맷의 버전과 변형 종류를 나타냅니다. 여기서 SYSV는 System V 유닉스 규격을 뜻한다.
- not stripped
- 디버깅 정보나 심볼 테이블 등의 부가 정보가 파일 내에 그대로 포함되어 있는 상태.
- 참고로 "stripped" 상태는 파일 용량 최적화를 위해 이러한 부가 정보를 제거한 것을 의미.
(.o)파일과 실행파일 비교하기

gdb 사용해 보기
명령어 | 설명 |
gcc -o char_overflow char_overflow.c -g | 디버깅 심볼( -g)을 포함하여 프로그램 컴파일 |
gdb char_overflow | GDB 디버거로 해당 프로그램 열기 |
b main | main 함수에 중단점(Breakpoint) 설정 |
r | 프로그램 실행 (Run) |
n | 다음 줄 실행 (Next) |
p value | p [변수명] : 변수값 출력 (Print) |
p/t value | p /[출력형식] [변수명] : 지정한 형식에 맞추어 변수값 출력
(여기서 t는 2진수 의미) |
b main r


n p

p/t value

