-
[운영체제 만들기] 코딩 컨벤션프로젝트/운영체제 만들기 2023. 5. 16. 15:27
글의 참고
- 주 참고 글
-
- 보조 참고 글(`주 참고 글`을 이해하기 위한 참고한 글)
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
- `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.
- `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.
글의 내용
- 정수형 변수는 되도록 이면 양의 정수가 아닌, 일반 정수형 타입 사용하기.
: yohdaOS의 거의 대부분은 함수들은 에러 타입을 반환하는데, 이 에러 타입은 음수다. 그런데, unsigned 정수형 타입을 사용하면 이 에러를 인식하지 못하게 된다. 예를 들어, 아래와 같이 함수 foo()는 unsigned int를 받는데, 즉, 양의 정수다. 그래서 -2같은 값을 받게되면 foo()의 파라미터 a는 -2를 양의 정수로 컨버팅해서 4억 2천 언지리 값으로 해석한다. 그래서 디버깅이 매우 어려워진다.
#include <stdio.h> int foo(unsigned int a) { if(a < 0) return a; a += a; return 0; } int main() { int err = -1, temp = 0; temp = -2; err = foo(temp); if(err < 0) return err; return 0; }
: 고로 거의 모든 정수형 타입은 대개 양의 정수가 아닌 정수형 타입을 사용하도록 한다.
- 포인터를 전달하는 함수는 반드시 자신의 영역안에서 할당/해제를 해야 한다.
: 포인터를 사용하면 메모리릭의 시작이라고 볼 수 있다. 그러므로, 언제/어디서 메모리를 할당했는지 쉽게 추척할 수 있어야 한다. 그래서 포인터가 함수의 인자로 전달될 경우, 자기 자신이 메모리를 할당해서 전달하도록 한다.
- 함수의 반환값은 반드시 에러 타입(정수형)을 반환해야 한다.
- 포인터가 들어간 구조체는 반드시 포인터를 사용하기 전에 초기화한다.
: 너무나 당연한 얘기지만, 포인터가 포함된 구조체는 멤버 변수인 포인터에 메모리를 할당하기 전에 초기화를 진행해야 한다. 만약 구조체의 멤버 변수로 포인터가 있는데, 해당 포인터가 메모리를 할당받고 해당 구조체를 초기화를 하면 할당받은 메모리를 메모리릭으로 영영 사용 불가능할 수가 있다.
#include <stdio.h> #include <stdlib.h> struct temp { int *ptr; int a; }; int main() { struct temp yohda; yohda.ptr = malloc(sizeof(int)*32); printf("0x%x\n", yohda.ptr); // 0x62F3_0021 memset(&yohda, 0, sizeof(yohda)); printf("0x%x\n", yohda.ptr); // 0x0000_0000 return 0; }
: 올바르게 한다면 아래와 같이, 전체적으로 구조체 변수를 초기화하고 나서 해당 멤버 변수인 포인터를 초기화 해야 한다.
#include <stdio.h> #include <stdlib.h> struct temp { int *ptr; int a; }; int main() { struct temp yohda; memset(&yohda, 0, sizeof(yohda)); printf("0x%x\n", yohda.ptr); yohda.ptr = malloc(sizeof(int)*32); memset(yohda.ptr, 0, sizeof(int)*32); printf("0x%x\n", yohda.ptr); // 0x0000_0000 return 0; }
- 함수로 전달할려는 변수들은 전달하기 전 반드시 초기화한다. 함수안에서 전달받은 인자를 함부로 초기화하지 않는다.
- void* 포인터 보다는 unsigned char*를 사용한다.
: 포인터 변수에 앞에 `*` 기호를 쓰면 해당 주소의 데이터를 출력한다. unsigned int* 로 선언된 변수에 앞에 `*`을 쓰면 unsigned int를 출력한다. void* 변수는 어떨까? void를 출력한다. void는 데이터 타입은 주소 지정이 불가능한 값이다. 그래서 값도 없고 연산도 불가능하다. 이렇게 void*를 사용하면 오류가 발생할 확률을 높인다(사실 이 부분은 애매하다. void*에 `*`을 붙이면, 컴파일 타임에 잡을 수 있는 에러라서 어떠면에서는 좋은 부분이 많다). 객체 지향적인 관점에서 void*는 유연성을 제공하지만, 안전성이 좋아지지 않으므로, void* 연산을 해야 하는 경우 그와 동일한 unsigned char*으로 대체한다.
- 가변으로 사이즈가 늘어날 변수들은 리스트로 선언한다.
: 예를 들어, 파일 시스템에서 각 폴더들은 여러 개의 파일을 가질 수 있다. 이럴 때, 각 파일들을 배열로 선언하거나 포인터로 동적 할당을 받을 수 있다. 그러나 배열은 정적이라서 공간 낭비가 올 수도, 오히려 부족할 수도 있다. 상황을 예측할 수 없을 때는 배열은 안된다. 그러면 포인터로 동적 할당을 할 수가 있는데, 이건 새로 할당받은 주소에 이전 메모리를 복사해야 과정으로 코드가 지저분해 지거나, 아예 메모리를 할당받지 못하는 경우가 발생할 수 있다.
: 예를 들어, 32K를 사용하는데 4K가 더 필요해서 36K를 할당받아여 하는 경우 아예 새로운 메모리를 할당받아서 이전 메모리를 새로 할방받은 주소로 복사해야 한다. 만약 동일한 주소에서 4K를 늘려서 받는다면 문제가 없겠지만, 메모리 관리 기법을 공부해봤다면 그럴 경우는 굉장히 적거나 아예 없다고 가정하는게 정신 건강에 이롭다.
: 그리고 포인터의 동적 할당은 일반적으로 정렬된 메모리를 할당받아야 한다. 포인터가 배열처럼 쓸 수 있는 이유는 할당받은 메모리가 정렬되어 있기 때문이다. 즉, 위의 예에서 36K를 새로 할당받아야 할 경우, 32K는 0x23FD 시작주소로 4K는 0xFD24로 받을 수 없다는 뜻이다.
: 리스트는 메모리가 연속적일 필요가 없다. 그래서 현재 노드의 주소가 0x23FD인데 다음 노드의 주소가 0xFD24일 수가 있다. 그래서 메모리를 효율적으로 사용할 수 있다. 그러므로, 가변적으로 동일한 사이즈가 늘어날 경우 배열 및 포인터 대신 리스트를 사용하자.
: YohdaOS에서 대표적인 예시로 struct fat_file이 있다.
- 포인터의 남용보다는 데이터를 복사해서 쓴다.
: 포인터를 이용하면 여러 변수들이 한 메모리에 직접 접근이 가능하다. 그리고 포인터는 데이터 복사가 아니라, 메모리 주소에 대한 주소만 복사하므로 속도 또한 빠르다. 이러한 이점에도 불구하고 거의 모든 코드를 포인터로 도배하면 코드가 짧더라도 복잡해지는 이상한 현상을 겪게된다. 왜냐면, 함수나 블락 괄호들은 메모리 주소에 대한 경계을 만들어준다. 즉, 이 영역안에서만 의미가 있고 이 영역밖에서는 의미가 없어진다. 그러나 포인터는 이런 경계를 없애버린다. 왜냐면, 포인터는 블락 및 함수에 영향을 받지 않고 메모리를 직접적 가리키기 때문이다. 마치 전역 변수와 같다.
: 이게 왜 문제가 되느냐고 붙는다면, 인터럽트와 멀티 코어 프로세서 때문에 문제가 된다.
- 자주 값이 많이 바뀌는 변수는 포인터로 선언한다.
: 1KB의 이상이 사이즈를 가지는 변수가 자주 값이 변경되야 한다면, 포인터로 선언하는 것이 효율적이다. YohdaOS의 FAT 파일 시스템에서 fmm.cd 변수가 그예이다.
- 구조체 멤버 변수로 포인터가 존재하고, 값이 변하는게 아닌 크기가 자주 변해야 한다면(가변적이라면) 리스트로 선언한다.
: 구조체 변수로 포인터가 있으면, 버그가 발생할 확률이 높아진다. YohdaOS에서 초창기에 struct fat_dir 구조체안에 struct fat_file *fles라는 멤버 변수가 있었다. 그러나, 파일 시스템에서 디렉토리안에 파일의 개수는 굉장히 가변적으로 변한다. 그러므로, 포인터보다는 리스트로 방식을 바꿨다.
// Before // struct fat_dir { ... struct fat_file *files; ... }; // After // struct fat_dir { ... struct list_node *ll; ... };
: 대신 위와 같이 리스트로 바뀔 경우, struct fat_file에 struct list_node가 추가되어야 한다.
// Before // struct fat_file { ... ... }; struct fat_file { ... struct list_node ll; ... }
- 값이 거의 변하지 않는다. 그런데 크기가 가변적이다. 거기에 해당 데이터의 사이즈가 매우 작다면, 포인터로 선언해서 메모리 재할당을 고려한다.
: YohdaOS에서 대표적인 예시로 struct fat_file의 멤버 변수인 int *clus이다. 파일 및 폴더 마다 클러스트의 개수는 가변적이다. 그런데 클러스터 하나만 리스트로 연결하기에는 메모리 낭비가 있을 것으로 보인다. 리스트는 포인터 2개를 사용해서 8바이트인데, 클러스터 하나를 저장하기 위한 데이터는 고작 4바이트 이기 때문이다. 즉, 메타 데이터가 실제 데이터보다 큰 셈이다. 이럴 경우, 데이터가 작지만, 상당히 가변적일 확률이 높으므로 포인터로 선언하되 할당된 메모리를 모두 소진하면 재할당(realloc)을 통해 사이즈르 늘린다.
- 함수내부에서 선언된 변수의 데이터가 외부로 노출되어야 하는 경우, `데이터 복사` 혹은 `동적 할당`을 고려 한다.
: 아주 기초적인 내용이지만, 몇 가지 짚고 넘어갈 부분이 있다. 기본적인 함수 사용시 스택에 대한 주의점은 크게 2가지가 존재한다.
1" 함수내에 스택은 단지 SP 포인터를 옮김으로써, 해당 함수의 스택 프레임을 제거한다. 즉, 데이터를 0으로 초기화하지 않고 단지 SP만 옮기므로 다음에 그 스택을 사용하는 함수에서는 반드시 지역 변수들의 값을 초기화해서 사용해야 한다.
2" 위에서 언급된 부분의 연장으로 함수의 스택은 SP만 옮김으로써 스택을 정리하기 때문에, 만약 함수내에서 만들어진 변수를 전역 변수에 연결하면 함수가 끝나더라도 해당 내용이 사라지지 않고, 있다가 그 스택이 언젠가 누군가에게 할당되어 초기화되면 갑자기 값이 사라지는 일이 발생할 수 있다. 이건 디버깅도 굉장히 어렵기 때문에 함수내에서 외부로 값을 노출해야 하는 경우 반드시 `데이터 복사`를 한다.- Stack내에 선언한 변수들은 사용 전 반드시 초기화를 해야 한다.
: yohdaOS에 FAT32 파일 시스템을 추가하는 과정에서 뭔가 값이 좀 이상했다. 그래서 확인해보니 변수를 초기화하지 않아서 발생한 문제였다. 스택 프레임을 정리하는 과정은 스택을 0으로 초기화하고 SP를 옮기는게 아니라, 단지 SP만 옮긴다. 그래서 스택에서 변수를 선언하고 초기화하지 않고 값을 출력하면 이상한 값이 나올 수 있다. 이 문제는 스택만의 문제가 아니다. 모든 메모리 해제하는 과정은 해당 메모리를 초기화하고 할당하는 것이 아니다.
: 위에서 memset()을 기준으로 tmps 구조체를 초기화하기전 출력하고 초기화를 한 후 출력해볼 것이다. 해당 내용은 아래와 같다.
: tmps 변수의 주소는 0x6FFC78F다. 사이즈는 32바이트다. 그리고 값은 해당 구조체에 할당된 32바이트를 출력하는 내용이다. memset(tmps, 0x00, 32); 전에 값이 쓰레기가 값인 것을 참고하자. 모든 변수를 사용하기 전에 반드시 초기화하자.
- Stack 내에 선언한 변수를 포인터에 연결하면 안된다.
: 아래 코드의 문제가 뭘까? printf() 문에서 4를 정상적으로 잘 출력된다. 목적만 보면, 이 코드는 4를 잘 출력하고 끝낸다. 그러나 코드가 길어지면 어떻게 될까? 디버깅 조차 굉장히 어려워지는 큰 문제를 발생시킨다. 왜냐면, 사실 코드 자체에 명백한 문제가 존재하고 있는데, 이 문제가 나중에 특정 조건이 충족될 때 발생하기 때문이다. 근데, 해당 조건이 발생을 할 수도 있고, 안할 수도 있다.
#include <stdio.h> int test(int **x) { int a = 4; *x = &a; } int main() { // Write C code here int *a = NULL; test(&a); printf("%d\n", *a); // 4 return 0; }
: 위 코드의 문제는 지역 변수의 주소를 포인터가 가리키고 있다는 문제가 있다. 이건 시한 폭탄과 같다. 왜냐면, 함수가 종료될 때 SP만 변경하기 때문에 이 값이 test() 함수 종료 시점에 할 수 없기 때문이다. 그래서 결론적으로는 주의해야 한다는 말밖에는 할 수가 없다.
'프로젝트 > 운영체제 만들기' 카테고리의 다른 글
[운영체제 만들기] Exception (0) 2023.05.25 [운영체제 만들기] 에러 사항 (0) 2023.05.24 [운영체제 만들기] 문자열 - 라이브러리 (0) 2023.05.18 [운영체제 만들기] VBR (0) 2023.03.26 [운영체제 만들기] MBR (0) 2023.03.26