Tsoding 유튜브의 C관련 내용 정리
C언어 관련 자잘한 정보들
C 자료구조 공부를 하려고 자료를 찾아보다가, 예전부터 이름만 알고 있던 Tsoding 영상을 몇 개 보게 됐다.
C를 쓰다보면 필요한 유용한 정보들이 많아서 메모해두려고 한다.
항상 아쉬운점은 Tsoding Daily 채널의 영상의 자막은 트위치 댓글을 보여주는 자막이라서, 자동 생성 자막을 봐야할 때가 있다.
영어권 사용자에게는 큰 문제가 아닐 수 있지만, 영어를 못하는 사람에게는 꽤나 불편함;;
정리는 안하고 링크만 적을거
- Why Linux Has This Syscall?!: mmap 시스템 콜에 대한 사용 영상
- Writing My Own Malloc in C: 직접 Malloc 구현하는 영상
thread 생성이나, gblic의 malloc에서도 mmap를 사용하니까 봐두면 좋지 않을까... (관련 자료: Practical libc-free threading on Linux )
1. Dynamic Arrays in C
Dynamic Arrays를 사용하는 방법을 소개한다.
count와 capacity를 구조체로 묶어두면 동적 배열의 상태를 한 번에 관리할 수 있다. 이런 식으로 items, count, capacity 패턴을 공통화해두면, 필요할 경우 매크로나 별도 래퍼를 이용해 다른 타입에도 같은 방식으로 확장할 수 있다.
구조 자체는 count와 capacity를 비교해서 확장하는 연산으로 평범함.
예를 들어 int 대신 float를 저장하는 구조체를 따로 만들고 같은 로직을 적용(매크로를 사용해서 재사용)하면, 동일한 방식의 동적 배열을 다른 타입에도 사용할 수 있다.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *items;
size_t count;
size_t capacity;
} Numbers;
int main(void)
{
Numbers xs = {0};
for (int x = 0; x < 10; ++x) {
if (xs.count == xs.capacity) {
xs.capacity = (xs.capacity == 0) ? 4 : xs.capacity * 2;
xs.items = realloc(xs.items, xs.capacity * sizeof(*xs.items));
}
xs.items[xs.count++] = x;
}
for (size_t i = 0; i < xs.count; ++i) {
printf("%d\n", xs.items[i]);
}
free(xs.items);
return 0;
}2. Libraries That Quietly Revolutionized C
STB 스타일의 single-file library 를 소개하는데, C의 불편함을 정면으로 부정하는 대신 오히려 C의 전처리기와 번역 단위 모델을 이용해서 단순한 배포 형식을 만든 사례를 설명한다.
라이브러리를 헤더/소스 여러 파일로 쪼개지 않고 하나의 헤더 파일로 배포한다. 그리고 특정 .c 파일 한 곳에서만 STB_IMAGE_IMPLEMENTATION 같은 매크로를 정의해 실제 구현을 끌어오고, 나머지 곳에서는 선언만 보이게 만든다. 이 방식은 C의 #include 가 사실상 텍스트 복사라는 점, 선언과 정의를 분리해야 하는 컴파일 모델을 정확히 이용한 패턴이다.
STB 스타일 single-file 라이브러리는 C를 더 편한 언어로 바꾼 것이 아니라, C의 전처리기와 번역 단위 모델을 활용해 배포를 단순화한 패턴이다. 사용자는 헤더 하나만 포함해 쉽게 시작할 수 있고, 필요하면 구현을 별도 번역 단위에서 컴파일해 정적 라이브러리처럼 연결할 수도 있다.
또 하나 인상적인 지점은 “single-file”이 꼭 작은 코드라는 뜻은 아니라는 점이다. 영상에서는 MiniAudio 같은 큰 라이브러리도 예시로 언급되는데, 이건 단일 파일 배포가 단순히 취미성 트릭이 아니라 배포와 통합 비용을 최소화하는 실용적 설계라는 걸 보여준다.
npm 같은 무거운 의존성 생태계와 대비해서 보면, C 진영이 아직도 특정 종류의 단순함에서는 매우 강하다는 얘기이기도 하다.
3. How to Prevent All Memory Leaks
메모리 할당/해제는 추상화된 개념임. 따라서 메모리 Leak을 정의하기 어렵고 잡기도 어려움.
free를 안하고 프로그램이 끝나면서 지워지는게 문제는 아님. 의도하지 않게 free가 안되는게 Leak임.
GC가 있다고 해도 이런 문제는 생길 수 있음(reference cycle). 특히 C같은 시스템 언어에선 포인터 연산이 가능해서 unreachable memory 모델이 유효하지 않을 때도 있음.
AI 요약
1. Valgrind와 ASan은 둘 다 쓴다
Tsoding은 메모리 릭을 잡을 때 둘 다 쓴다고 말한다. 다만 Valgrind로 메모리 릭을 잡는 건 어렵다고 본다. 이유는 “메모리 릭” 자체의 정의가 흐리기 때문이라고 한다.
2. “종료 전까지 free 안 된 메모리”라는 정의는 별로 유용하지 않다
프로그램이 실행 내내 일정한 메모리를 잡고 있다가 종료 시점까지 해제하지 않는 경우가 흔하고, 이런 건 기술적으로는 릭처럼 보일 수 있어도 실제 문제를 일으키지 않을 수 있다고 말한다. 그래서 이 정의는 실질적으로 쓸모없다고 본다.
3. “도달 불가능한 메모리”라는 정의가 더 낫지만, 이것도 완전하지 않다
GC 관점에서 unreachable memory라는 정의가 더 낫다고 말한다. 하지만 시스템 언어처럼 포인터를 직접 다루는 경우에는 이것도 문제가 있다. 포인터를 변형하거나 특이한 방식으로 다루면 GC가 참조를 제대로 인식하지 못할 수 있기 때문이다.
4. D 언어의 GC 예시
D는 GC가 있지만 시스템 프로그래밍 언어라 포인터를 직접 다룰 수 있다. 그래서 메모리를 스캔하면서 포인터처럼 보이는 정수를 참조로 간주하는데, 포인터를 비정상적인 방식으로 조작하면 GC가 실제로는 살아 있는 객체를 unreachable하다고 오판해서 정리할 수 있다고 설명한다.
5. XOR linked list 예시
XOR linked list처럼 두 포인터를 XOR해서 저장하면, 그 값은 정상적인 포인터처럼 보이지 않는다. 그래서 D의 GC는 이런 구조를 제대로 추적하지 못하고, 실제로 필요한 객체를 없애버릴 수 있다고 말한다. 그래서 D 개발자들은 이런 식의 트릭을 쓰지 말라고 권장한다고 한다.
6. JavaScript에서도 메모리 문제는 생긴다
JavaScript처럼 GC가 있는 언어에서도 콜백 정리를 잊으면 객체가 계속 reachable 상태로 남을 수 있다고 말한다. 즉, GC가 있어도 원래는 사라져야 할 객체가 계속 살아남아 메모리 사용량이 늘어날 수 있다는 것이다.
7. 결론: 메모리 릭은 객관적으로 딱 잘라 정의하기 어렵다
화자는 메모리 릭이 매우 주관적이고, 깊게 생각해 보면 단순히 “free를 안 했다” 수준의 문제가 아니라고 말한다. 오히려 의도와 맥락이 섞인 애매한 개념에 가깝다고 본다.
8. 메모리 할당 자체가 인공적 구성물이라는 주장
후반부에서는 메모리 할당/해제가 계산 모델의 본질적인 개념이라기보다, 실제 프로그래밍을 쉽게 하려고 위에 덧씌운 인공적 구조라고 말한다. 그래서 지금의 메모리 문제들은 우리가 만든 추상화 위에서 생기는 문제라고 설명한다.
4. Stop telling me about _Generic
근데 그렇게 따지면 Java의 제네릭도 제네릭이 아니라고 보는건가? 아니면 AI가 잘못 평가한건가? Java도 따지면 오버로딩 느낌인데.
AI 요약
1. _Generic은 generics가 아니라 타입 기반 선택기다
Tsoding은 C의 _Generic을 generics라고 부르는 건 잘못됐다고 본다. 이 기능은 전통적인 의미의 제네릭이 아니라, 타입에 따라 이미 만들어져 있는 표현식이나 함수를 골라주는 장치에 가깝다고 말한다.
2. 함수 오버로딩과 비교
C는 같은 이름에 다른 시그니처를 가진 함수를 둘 수 없지만, C++은 가능하다. 다만 내부적으로는 같은 이름을 그대로 쓰는 게 아니라, 컴파일러가 타입 정보를 이름에 섞어 넣는 name mangling으로 서로 다른 심볼로 구분한다고 설명한다.
3. _Generic는 오버로딩과 비슷한 구조
Tsoding은 _Generic이 하는 일도 본질적으로는 비슷하다고 말한다. 예를 들어 foo_int, foo_float처럼 타입별 함수를 따로 만들어 두고, _Generic이 인자의 타입을 보고 그중 하나를 선택하게 하면 된다. 그래서 이 기능은 수동으로 mangling한 함수들 사이에서 골라주는 오버로딩 흉내일 뿐이라고 본다.
4. 핵심 차이는 dispatch와 generate의 차이다
여기서 Tsoding은 중요한 구분을 한다. 함수 오버로딩은 이미 존재하는 코드 중에서 어떤 것을 호출할지 결정하는 dispatch이고, generics는 타입에 맞는 코드를 새로 만들어내는 generate라는 것이다. 즉, 선택과 생성은 전혀 다른 문제라고 본다.
5. C++ 템플릿과 Rust generics와의 비교
이 차이를 설명하기 위해 C++ 템플릿과 Rust generics를 예로 든다. 둘 다 같은 함수 형태를 여러 타입으로 호출하면, 컴파일러가 타입별 버전을 실제로 따로 만들어낸다고 설명한다. 특히 Rust는 trait bound 같은 제약도 줄 수 있어서, Tsoding은 이를 더 전형적인 generics의 예로 본다.
6. 결론: _Generic은 진짜 generics라고 보기 어렵다
Tsoding의 결론은 단순하다. C의 _Generic은 타입별 코드를 생성하지 않고, 기존 코드 중 하나를 고르는 기능만 제공한다. 그래서 “C에는 이미 generics가 있다”, “_Generic 쓰면 된다” 같은 말은 부정확하다고 비판한다.
5. You should read Open Source code
동적 라이브러리를 어떻게 사용하는데 궁금했는데, 우연히 알게 됨. 근데 이게 어떻게 접근 가능한건지는 모르곘음. 그냥 read only로 OS가 강제하고 읽어서 쓰나?
그건 뭐... 나중에 알아보기
AI 요약
1. 결론: 구현 아이디어는 오픈소스를 읽으면서 배울 수 있다 (결론을 먼저 나오게 위치 변경. 영상에서는 맨 뒤에 나옴.)
Tsoding은 이런 저수준 테크닉을 어디서 배우냐는 질문에, 특별한 비법서나 강의보다 오픈소스 코드를 직접 읽고 디버깅하는 과정이 가장 큰 학습원이라고 말한다. 문서만 보면 지나치기 쉬운 구현상의 트릭이나 설계 의도를 실제 코드에서 더 잘 볼 수 있다는 것이다.
2. 이번 예시는 SDL 소스코드를 읽다가 발견한 아이디어다
그가 소개하는 사례는 SDL2 코드를 보다가 알게 된 것이다. SDL2가 동적 라이브러리와 심볼을 다루는 방식을 따라가다 보니, dlopen과 dlsym을 이용해 꽤 흥미로운 일을 할 수 있다는 점을 발견했다고 설명한다. 즉, 이번 내용은 추상적인 개념 설명이 아니라, 오픈소스를 읽다가 실제 구현 아이디어를 포착한 사례로 제시된다.
3. 기본 개념: dlopen은 런타임에 공유 라이브러리를 여는 기능이다
먼저 Linux의 dlopen은 동적 라이브러리를 실행 중에 로드하는 기능이라고 설명한다. 보통은 .so 파일 경로를 넘겨 라이브러리를 열고, 그 결과로 핸들을 얻는다. 그리고 dlsym을 통해 그 라이브러리 안의 함수 심볼을 찾아 사용할 수 있다.
4. dlsym으로 찾은 함수는 함수 포인터로 바로 호출할 수 있다
예를 들어 raylib를 dlopen으로 연 다음, InitWindow 같은 심볼을 dlsym으로 찾으면 그 주소를 함수 포인터로 받아 실행할 수 있다고 설명한다. 즉, 정적으로 링크하지 않았더라도, 런타임에 라이브러리를 열고 함수 주소를 찾아 실제 기능을 호출할 수 있다는 것이다.
5. 이런 방식은 런타임에 환경을 보고 백엔드를 고르는 데 유용하다
Tsoding은 이 메커니즘이 동적인 애플리케이션에서 유용하다고 본다. 실행 환경에 raylib가 있으면 raylib를 쓰고, 없으면 SDL을 쓰는 식으로, 실행 시점에 무엇이 가능한지 확인한 뒤 그에 맞춰 동작을 바꿀 수 있기 때문이다.
6. 여기서 재미있는 포인트는 dlopen(NULL, ...)이다
그가 특히 흥미롭게 본 것은, dlopen에 라이브러리 경로 대신 NULL을 넣을 수 있다는 점이다. 이렇게 하면 외부 라이브러리가 아니라 현재 실행 중인 프로그램 자신을 열게 된다고 설명한다. 처음에는 이상하게 들리지만, 실행 파일과 공유 객체가 모두 ELF 포맷이라는 점을 생각하면 그렇게 놀랄 일은 아니라는 것이다.
7. 그러면 프로그램이 링크한 라이브러리의 심볼을 자기 자신을 통해 찾을 수 있다
프로그램 자신을 연 뒤 dlsym을 쓰면, 현재 실행 파일이 의존하고 있는 라이브러리들의 심볼도 조회할 수 있다고 설명한다. 예를 들어 malloc을 찾아 보면, 헤더를 통해 접근한 malloc과 dlsym으로 얻은 주소가 같다는 점을 확인할 수 있다. 즉, 자기 자신을 열어도 결국 현재 프로세스가 가진 심볼 공간을 탐색하게 된다.
8. 하지만 자기 프로그램 안의 함수는 기본적으로 바로 찾을 수는 없다
Tsoding은 여기서 한 걸음 더 나아가 main도 찾을 수 있는지 시험한다. 기본 상태에서는 되지 않는다고 설명한다. 이유는 실행 파일의 자체 심볼들이 기본적으로 동적 심볼 테이블에 모두 들어가지는 않기 때문이라고 본다.
9. -rdynamic을 쓰면 자기 함수도 노출된다
이 문제는 링크 옵션 -rdynamic으로 해결할 수 있다고 설명한다. 이 옵션을 주면 실행 파일의 심볼도 동적 심볼 테이블에 포함되므로, dlsym으로 main 같은 자기 프로그램의 함수까지 찾을 수 있다. 그리고 이렇게 얻은 주소가 실제 main의 주소와 일치한다는 점도 보여준다.
10. 심지어 main을 다시 호출할 수도 있다
찾아낸 main을 함수 포인터처럼 호출하는 것도 가능하다고 말한다. 물론 그렇게 하면 main이 다시 자기 자신을 부르며 재귀로 들어가게 되므로 실용적이진 않다. 그래도 실행 파일도 ELF 객체라는 점을 생각하면, 기술적으로는 가능한 동작이라는 것이다.
11. SDL도 이런 기법을 실제로 활용한다
이게 단순히 장난 같은 실험으로 끝나는 건 아니라고 설명한다. SDL2는 실제로 글로벌 심볼이나 연결된 라이브러리의 특정 심볼을 조회해서, 어떤 환경이나 드라이버가 있는지, 혹은 특정 프로그램에 대한 quirks가 필요한지 판단하는 데 이런 방식을 사용한다고 말한다.
12. 더 확장하면 libffi 같은 도구로 모르는 함수도 호출할 수 있다
후반부에서는 libffi도 언급한다. 이 라이브러리를 쓰면 동적 라이브러리에서 찾은 함수의 시그니처를 데이터로 기술해서, 컴파일 시점에 타입을 몰라도 런타임에 올바르게 호출할 수 있다고 설명한다. Tsoding은 이를 이용해 동적 라이브러리를 불러와 함수들을 호출하는 REPL 비슷한 도구도 만들었다고 말한다.