- 자료형 크기 나타내기
- 자료형 크기를 모르면?(오버플로우)
- 비트 연산 프로그래밍
- 코드가 실행파일이 되기까지
프로그래밍 언어에는 다양한 형태의 데이터를 해석하기 위해 자료형을 정의하고 있다.
정수를 나타내는 int, 문자를 타나내는 char가 그 예시이다.
그에따라 각 자료형은 다른 크기를 가지고 있다.
각 자료형의 크기를 한번 알아보자
#include <stdio.h>
int main(int argc, char* argv[]) {
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;
}
자료형 크기를 고려하지 않고 코드를 짜면, 의도하지 않은 버그가 발생할 수 있다.
가령, 자료형 char
는 1바이트로 -128 ~ 127
사이의 수를 나타낼 수 있다.
char val1 = 127
인 val
에 1을 더하면 어떤 일이 발생할까?
char val2 = -128
인 val
에 1을 빼면 어떤 일이 발생할까?
코드로 알아보도록 하자
#include <stdio.h>
#include <limits.h>
int main(int argc, char* argv[]) {
char value1 = CHAR_MAX;
printf("Original value1: %d\n", value1);
value1 = value1 + 1;
printf("Value1 after add 1: %d\n", value1);
char value2 = CHAR_MIN;
printf("Original value2: %d\n", value2);
value2 = value2 - 1;
printf("Value2 after sub 1: %d\n", value2);
return 0;
}
char
는 1바이트로 8비트의 크기를 가지고 있으며, 최상단 비트는 부호비트로 작용한다.
따라서 -128 ~ 127
범위의 수를 나타낼 수 있다
1000 0000 ~ 0111 1111
따라서 value1의 경우,
0111 1111 + 1 = 1000 0000
127 + 1 = -128
value2의 경우,
1000 0000 - 1 = 0111 1111
-128 - 1 = 127
이를 오버플로(Overflow)
라고 하는데, 이로 인한 취약점이 많이 발견되고 있다.
신경써서 코딩하자
특정 비트를 켜고 끄는 프로그램을 구현해보자
#include <stdio.h>
// 특정 위치의 비트를 1로 설정하는 함수
unsigned char set_bit(unsigned char value, int position){
return value | (1 << position);
}
// 특정 위치의 비트를 끄는 함수
unsigned char clear_bit(unsigned char value, int position){
return value & ~(1 << position);
}
int main() {
unsigned char value = 0b00001000; //value = 8 (0000 1000)
int position;
printf("position : ");
scanf("%d", &position);
position-=1;
// position번째 비트를 설정
value = set_bit(value, position);
printf("Value after setting %d bit: %d\n", position, value);
// position번째 비트 off
value = clear_bit(value, position);
printf("Value after clearing %d bit: %d\n", position, value);
return 0;
}
소스코드는 그 자체로 실행이 불가능하다. 컴퓨터는 기계어만 알아듣는다.
소스코드가 기계어로 변하는 과정 예제 프로그램으로 알아보자
#include <stdio.h>
int main(){
printf("Hello, world!\n");
return 0;
}
gcc -E main.c -o main.i
gcc -S main.i -o main.s
gcc -c main.s -o main.o
여기서부터 파일 내용이 확인이 안된다.
👉 기계어이기 때문에
file *
명령어를 통해 현재 디렉토리에 있는 파일들의 정보를 보자
- ELF(Excutable and Linkable Format): 윈도우에서 .EXE파일과 비슷
- 64-bit: 64비트 아키텍쳐로 컴파일
- LSB: Little Endian 바이트 순서 사용
- relocatable: 재배치 가능. 즉, 링커에 의해 다른 오브젝트 파일이나 라이브러리와 연결될 수 있음
- x86-64: x86-64(또는 AMD) 아키텍쳐용으로 컴파일 됨
- version 1(SYSV): ELF 포맷의 버전과 변형. SYSV는 System V 유닉스를 의미함.
- not stripped: "stripped"는 디버깅 정보나 심볼 테이블 등의 추가 정보를 제거한 상태를 의미.
"not stripped"는 이러한 추가정보가 오브젝트 파일에 포함되어 있음
링커에 의해 실행 가능한 바이너리나 라이브러리로 만들어 지기 전의 형태
gcc main.o -o main
부수적인 파일 정보가 붙었고, 파일의 크기도 달라졌다.
readelf와 objdump로 확인한 결과 많은 부분이 달라졌다.
- 링킹되면서 다른 파일과 많이 합쳐지고 재배치됨
- 실행에 필요한 함수들이 많이 생김
gdb를 통해 확인하면, 실제 메모리에 주소가 올라간 것을 확인할 수 있다