Template를 C 기본 예제로 바꿔준다.
VScode를 사용할 것이기 때문에 VScode에 체크 표시를 한 후 컨테이너를 생성한다.
실행을 누르면 뜨는 화면에서 왼쪽의 VScode -> YES 클릭하면 환경 세팅이 끝난다.
(굳이 VScode를 쓰지 않아도 됨)
자료형의 크기를 확인하는 코드를 작성 후 실행시킨다.
#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;
}
차례대로 gcc main.c -> ./a.out 명령어를 통해 위 코드를 실행시킬 수 있다.
정상적으로 출력되는 것을 확인할 수 있다.
#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;
}
char형의 표현 범위는 -128~127이므로 CHAR_MAX(=value)는 127이다.
value에 1을 더하면 128, 즉 표현 가능 범위를 벗어나므로 overflow가 발생하여
-128이 출력된다.
+) gdb
gcc -o overflow overflow.c -g에 대한 설명은 다음과 같다.
gcc -o 실행파일명(.out) 오브젝트_파일명(.o)
-g 옵션 : gdb에게 제공하는 정보를 바이너리에 삽입한다.
(-g 옵션을 사용하지 않고 gdb로 디버깅하면, 역어셈 → 어셈블리 코드로만 디버깅 가능)
b main 명령어를 통해 main 함수에 breakpoint를 설정한다.
추가로 b (line 번호) 명령어를 통해 특정 줄에 breakpoint를 설정할 수 있다.
r 명령어는 현재 파일인 overflow.c 파일을 실행시킨다.
n 명령어를 통해 breakpoint부터 한 줄씩 프로그램을 실행시킨다.
value = value + 1 코드가 실행되고 난 후 p/t value 명령어를 통해
value 값을 출력하면 overflow가 일어난 값인 -128이 출력된다.
underflow는 overflow와 정반대의 개념으로 표현 가능한 범위보다 더 작은 수를
출력하려고 할 때 발생한다.
#include <stdio.h>
#include <limits.h>
int main(){
char value = CHAR_MIN;
printf("Original value : %d\n", value);
value = value - 1;
printf("Value after adding 1 : %d\n", value);
return 0;
}
CHAR_MIN(=value)는 -128이다.
value에 1을 빼면 -129, 즉 표현 가능한 범위를 벗어나므로 underflow가 발생하여
127이 출력된다.
+) gdb
같은 방식으로 디버깅하면 underflow가 발생하여 127이 출력되는 것을 볼 수 있다.
특정 비트를 끄는 프로그램을 끄는 함수를 구현해야 한다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_bit_set(unsigned char value, int position) {
return (value & (1 << position)) != 0;
}
unsigned char clear_bit(unsigned char value, int position) {
return value & position;
}
int main() {
// 비트가 0번째부터 시작한다고 가정
// 끄고 싶은 비트의 번호를 입력
int position = 0;
printf("position : ");
scanf("%d", &position);
// shift & not operator
int position_not = ~(1 << position);
int a;
printf("number of bit you want to know : ");
scanf("%d", &a);
unsigned char value = 0b00001000;
if (is_bit_set(value, a)) {
printf("%drd bit is set!\n", a);
}
else {
printf("%drd bit is not set!\n", a);
}
value = clear_bit(value, position_not);
printf("value after bit set : %d", value);
return 0;
}
사용자에게 입력받은 position 값에 비트 이동 연산과 not 연산을 취해준 값을
position_not이라는 변수에 저장 후 position 대신 대입해주면 된다.
gcc 컴파일러의 기초 사용법에 대해 알아본다.
컴파일의 과정은 다음과 같다.
program.c --(전처리기)--> program.i --(컴파일러)--> program.s --(어셈블러)-->
program.o --(링커)--> 실행 파일
위의 과정을 따라 .i, .s, .o 파일을 각각 만들어준다.
helloworld.c
#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}
helloworld.i
helloworld.s
helloworld.o
.o 파일은 기계어이기 때문에 파일 내용 확인이 불가능하다.
이제 file * 명령어를 통해 파일 각각의 특징을 살펴본다.
ELF : 리눅스에서 실행 가능(Executable)하고 링크 가능(Linkable)한 파일의 형식을 ELF(Executable and Linkable Format)라고 한다.
64-bit : 바이너리가 64비트 아키텍처용으로 컴파일됨(아키텍처에 대한 추가적인 이해가 필요한 것 같음)
LSB : Little Endian 바이트 순서를 사용한다는 의미이다.
relocatable : 이 오브젝트 파일은 재배치 가능(링커에 의해 다른 오브젝트 파일이나 공유 라이브러리와 연결될 수 있다)라는 의미이다.
x86-64 : 오브젝트 파일이 x86-64 아키텍처용으로 컴파일되었음을 의미한다.
version 1(SYSV) : ELF 포맷의 버전과 변형을 나타낸다.(SYSV = System V 유닉스)
helloworld.o 파일의 ELF 헤더이다.
readelf -a helloworld.o 명령어를 통해 읽을 수 있다.
helloworld.s(어셈블리어 파일)의 내용이다.
helloworld의 main 함수를 disassemble 명령어를 통해 확인해보면 비슷하게 출력되지만 추가로 어떤 위치에서 어셈블리어의 작용이 발생하는지 또한 확인 가능한 것을 알 수 있다.