9월 10일에 진행이되었던 화이트햇스쿨 멘토님께서 강의 해주신 내용에 대한 과제를 작성했습니다.
구름은 현재 제공하는 서비스들이 정말 많아 되도록 검색어에 구름IDE 라고 검색해 사이트에 들어가주고난 뒤 로그인을 한다.
모든 컨테이너를 눌러 들어가주면 내가 생성했던 컨테이너들이 보인다. 여기서 오른쪽 위에 새 컨테이너를 눌러 컨테이너를 생성해준다.
나머지는 그대로 두고 현재 공부할 언어는 C언어이기에 소프트웨어 스택을 C언어로 선택하고 OS는 별다른 이유가 없으면 최신 Ubuntu를 선택한다. 그리고 추가 도구로 VScode도 추가해 코딩을 하는데에 간편하게 사용할 수 있게 한다.
이후 컨테이너를 생성을 누름르면 조금의 시간이 지난뒤 컨테이너가 생성이 된다. 만약 컨테이너 생성에 오류가 발생하면 로그아웃을 한 뒤 다시 로그인해 시도하면 컨테이너 생성중 멈춤 문제는 해결 될 것이다.
생성된 컨테이너에 들어가고나서 왼쪽 메뉴에서 vscode 아이콘을 눌러 실행해주면 src폴더와 README.md 파일이 보일 것이다. src폴더안에는 main.c라는 파일이 들어있고 README.md파일을 보면 간단한 단축키를 확인할 수 있다. 현재 ubuntu에서는 gcc라는 c언어 컴파일러가 내장되어있기에 main.c를 컴파일 해주기 위해서 gcc -o main main.c라고 명령어를 컴파일을 해준다. 참고로 처음 컨테이너를 실행할 때 밑쪽에 터미널이 보이는데 x를 눌러 없애주고 vscode에서 Ctrl+`을 눌러 vscode 내에서 터미널을 열어주면 편하게 사용할 수 있다.
리눅스에서 어떤 실행파일을 실행할 때에는 ./main 이런식으로 앞에 ./를 넣어줘야 실행이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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 bouble: %zu bytes\n", sizeof(long double));
printf("Size of pointer: %zu bytes\n", sizeof(void*));
return 0;
}
해당 코드를 타이핑하고 gcc로 컴파일 한 뒤 실행한 결과
위와 같은 결과가 나왔다.
C언어에서의 데이터타입에 대해 좀더 설명하자면 위와 같은 데이터 타입은 자신의 컴퓨터가 어떤 아키텍쳐를 사용하고 있냐에 따라 다르게 나타난다.
이를 표로나타내자면
| C Data Type | Typical 32-bit | Typical 64-bit | x86-64 |
|---|---|---|---|
| char | 1 | 1 | 1 |
| short | 2 | 2 | 2 |
| int | 4 | 4 | 4 |
| long | 4 | 8 | 8 |
| float | 4 | 4 | 4 |
| double | 8 | 8 | 8 |
| long double | - | - | 10/16 |
| pointer | 4 | 8 | 8 |
위와 같다 여기서 32-bit아키텍쳐와 64-bit 아키텍쳐에서 32와 64는 CPU 가 데이터를 처리하는 최소 단위이다. 그리고 Typical 64-bit와 x86-64과의 차이는 검색으로 찾아본결과 x86_64 아키텍쳐에서는 64비트 주소공가늘 사용해 더 많은 메모리를 직접 주소로 지정할 수 있기에 보다 더 큰 메모리용량을 지원하고 연산속도도 효율적으로 더 빠르게 처리한다는 차이가 있다라는것을 알았다.
우선 오버플로에 대해 알아보겠다. C언에서 int 자료형의 크기는 4byte이다. 그럼 총 32bit를 사용한다는 것이다. 여기서 착각을 하면 안되는것은 32bit를 사용하기때문에 int의 최대 값은 2^32일것이다라는 것인데. 이것은 틀린말고 int 에서 맨 처음 비트 즉 MSB는 부호를 나타내기위해 사용이 된다는 것이다(첫비트가 0이면 양수 1이면 음수). 그리고 그리고 첫비트를 제외했을 때 모든 비트가 0일 경우에는 0이 통상적이다 하지만 MSB와 함께 본다면 숫자 0은 -0, +0 즉 2개가 나오게 된다. 따라서 이것은 비효율적이기 때문에 0은 양수에 하나만 두고 MSB가 1일때 나머지 비트가 모두 0일경우에는 최소값이라고 둔다. 그렇다면 최대 값은 0111 1111 1111 1111가 된다. 하지만 여기서 1을 더해버리면 단순 비트연산을 하자면 1000 0000 0000 0000이 된다, 즉 최대값에서 1을 더했더니 최소값이 됐다는 것이다. 이렇게 값 커지다가 주어진 범위를 넘어 커진다면 의도된 값과 다른 값이 나와버리는 것이 오버플로우이다.
언더플로우는 무엇일까 언더플로우도 오버플로우와 흐름은 같다 1000 0000 0000 0000은 위와 같이 int의 최소값이라고 가정을 할때 이 값에서 -1을 하면 0111 1111 1111 1111이 된다, 즉 최소값에서 -1을 했더니 최대값이 됐다는 것이다. 이와같이 언더플로우는 값이 작아지다가 주어진 범위를 넘어 작아진다면 의도된 값과 다른 값이 나와버리는 것이다.
언더플로우에 대해 실습을 해보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}
해당 코드를 실행해 본결과

위와 같은결과가 나왔는데 설명했던 내용과 일치한 결과가 나왔다는것을 확인할 수 있다.
비트연산을 할 때 나오는 경우의 수는 4가지이다. |A|B| |:-:|:-:| |0|0| |0|1| |1|0| |1|1|
특정 위치의 비트를 끈다는 것은 해당위치의 비트는 1이라는 것이다. 그리고 (1 << position) 이라는 쉬프트연산을 통해 비교할것이다. 그렇다면 목표는 정해진다.
| A | B | |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
이 표와 같은 결과가 나와야 한다. A는 기존에설정된 비트고 B는 설정할 비트이다. 그러나 사실상 2, 3번째에서 서로 모순되는 결과를 모여준다. 따라서 기존 비트가 0일경우 해당 비트를 1과 비교하지 않는 다는 것을 가정해야한다.
| A | B | |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
결과적으로 이런 연산결과가 나와야한다. 이 결과가 나오는 것은 XOR연산이므로 XOR연산을 사용해 비트를 끄면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdbool.h>
// 특정 위치의 비트 값이 1인지 확인하는 함수
int is_bit_set(unsigned char value, int position) {
return (value & (1 << position)) != 0;
}
// 특정 위치의 비트를 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 = 0b00000000; // 예: 3번째 비트만 1입니다.
printf("초기 비트배열: 0000 0000\n");
bool done = false;
int num, position;
while(!done)
{
printf("1: 비트상태확인\n2: 특정비트 1로변경\n3: 특정비트 0으로 변경\n이외: 끝내기\n>");
scanf("%d", &num);
printf("비트위치\n>");
scanf("%d", &position);
switch(num)
{
case 1:
// 2번째 비트를 설정
if(value = is_bit_set(value, position))
{
printf("해당번째 비트는 1입니다.\n\n");
}
else
{
printf("해당번째 비트는 0입니다.\n\n");
}
break;
case 2:
value = set_bit(value, position);
printf("설정후 값: %d\n\n", value);
break;
case 3:
value = clear_bit(value, position);
printf("설정후 값: %d\n\n", value);
break;
default:
done = true;
}
}
return 0;
}
C언어가 실행파일이 되어가는데에 이러한 과정을 거친다.
flowchart LR
A[소스코드]
A1[헤더파일]
B[전처리된 소스코드 파일]
C[어셈블리어 파일]
A --- A1
A1 -->|전처리| B
B -->|컴파일| C
flowchart LR
A[어셈블리 파일]
B[실행파일]
C[오브젝트 파일]
A -->|어셈블리| C
C -->|링킹| B
gcc를 이용해 바로 실행파일을 만들지 말고 단계를 거쳐가며 과정을 살펴보겠다.
gcc -E test.c -o test.i를 해 test.i의 파일은 전처리를 한 후의 파일이다.

이 파일은 사용한 라이브러리의 함수들을 다 불러오고 #define과 같은 것들도 해당 값으로 치환되며, 조건부 컴파일, 프로그램 모듈화 등을 한다. 이것을 컴파일 하면 전처리를 한 파일을 통해 짯던 코드를 어셈블리어로 바꾼다(gcc -S test.c -o test.s).

다음으로 이 어셈블리어의 코드를 이용해 어셈블링해서 아직까지는 사람이 읽을 수 있지만 기계어로 변환해 목적파일을 만든다(gcc -c test.s -o test.o).

ppt에 나와있는 것처럼 목적파일의 주요특징은 다음과 같다.
- 바이너리 형태: 오브젝트 파일은 기계어 코드를 포함하는 바이너리 파일이이다. 그러나 이 파일 자체로는 실행될 수 없다. 다른 오브젝트파일이나 라이브러리와 링크되어야 실행 가능한 바이너리가 된다.
- 재배치 가능: 오브젝트 파일은 다른 오브젝트 파일이나 라이브러리와 링크되어 완전한 프로그램을 형성할 수 있다. 이 과정에서 링커는 각각의 변수나 참조를 올바른 메모리 주소나 올바른 함수로 연결한다.
- 심볼 테이블: 오브젝트 파일에는 심볼 테이블이 포함되어 있다. 이는 파일 내의 각 함수나 변수의 이름과 위치 정보를 나타낸다. 이 테이블은 링커가 다른 오브젝트 파일이나 라이브러리와 해당 오브젝트 파일을 링크할 때 참조된다.
이후 목적파일을 링킹시키면 최종적으로 실행파일이 만들어진다.
64bit 아키텍쳐와 x86_64의 차이- https://ts2ree.tistory.com/355