1. 문법 함정

C/C++에서 연산자로 쓰이는 토큰(문자)들 중에는 문맥에 따라서 의미가 중복될 수 있는 것이 있다.
예를 들어 * () [] 같은 토큰은 값을 계산하는 수식에서 쓰일 때와, 변수를 선언할 때 의미가 서로 다르다. 한쪽에서는 인근의 변수가 배열· 포인터· 함수 타입임을 나타내지만, 다른 쪽에서는 실제로 배열 첨자나 포인터를 역참조하고 함수를 호출하는 역할을 한다.

심지어 =조차도 int a=5; 와 그냥 a=5; 에서 =는 문법적인 의미가 서로 동일하지 않다. 똑같이 =를 썼더라도 중괄호를 동원하여 배열이나 구조체를 초기화하는 것은 일반 수식에서는 가능하지 않기 때문이다.

이런 것 말고도 콤마(,)의 경우.. 함수 인자 구분자와 쓰임이 완벽하게 겹친다. 그렇기 때문에 함수 인자에서 콤마 연산자를 쓰려면 수식을 괄호로 싸야 한다.

그리고 <>로 둘러싸인 템플릿 인자에서 부등호 내지 비트 이동 연산자를 쓸 때도 상황이 좀 난감해진다. 템플릿 인자에 typename만 올 때는 <>가 모호성을 전혀 일으키지 않지만, 문제는 템플릿 인자로 정수도 들어올 수 있다는 것이다. 그리고 값이 컴파일 타임 때 결정만 될 수 있다면 정수값을 만들어 내는 각종 연산자들도 당연히 쓰일 수 있다.

template class Foo<size_t N> { .... };

Foo<(a>b ? 5:3)> bar1;
Foo<(MAX>>3)> bar2;

그러니 위와 같은 상황에서는 수식 전체를 괄호로 싸야만 한다. 괄호가 단순히 같은 수식 안에서 연산 우선순위를 조절할 때만 쓰이는 게 아니라는 점이 흥미롭다. 수식 영역과 함수 및 템플릿 인자 영역을 구분할 때도 쓰인다.

std::vector<std::list<int>> vl;

요렇게 중첩되었던 템플릿 인자들이 한꺼번에 종결될 때 > 사이를 강제로 띄우지 않아도 되게 컴파일러의 동작 방식 지침이 달라진 때가 내 기억이 맞다면 C++03과 C++11사이였지 싶은데.. 정확하게는 모르겠다.

그 밖에 2[a]가 가능하다는 C/C++의 변태적인 특성상, 람다와 관련해서 또 변태 같은 중의성을 만들 수 있지는 않으려나 궁금한데, 너무 머리가 아파서 더 생각해 보지는 않으련다.
요즘 C++은 auto라든가 using, delete를 보면 =를 사용하는 새로운 문법이 여럿 생긴 것 같다.

2. 비표준이지만 표준처럼 쓰이는 함수

C언어 라이브러리에 있는 모든 함수들이 100% 표준이고 어느 플랫폼에서나 동일하게 사용 가능한 게 아니다.
본인은 평소에 Visual C++만 쓸 때는 이런 걸 전혀 의식하지 않고 지냈는데.. strlwr과 심지어 내 기억이 맞다면 strdup도 macOS에서는 지원되지 않는 걸 최근에 확인하고는 놀랐었다.
물론 저런 함수들이야 하는 일이 워낙 간단하니 3분 만에 직접 짤 수도 있다. 하지만 핵심은 저건 universal한 표준이 아니라는 것이다.

Visual C++도 세월이 흐를수록 '표준 준수'를 강조하는 쪽으로 라이브러리의 디자인이 바뀌다 보니, 관례적으로 제공되긴 했지만 엄밀히 말해 표준이 아닌 함수들에 대해서는 앞에 밑줄을 붙여서 구분하는 추세이다.
하긴 그러고 보니, Visual C++을 업그레이드 한 뒤에 기존 코드가 컴파일되지 않아서 수정하던 내역 중에도 멀쩡한 함수 앞에다가 _를 붙이는 게 많았다. 일례로, 이분 검색 함수는 bsearch가 당당히 표준으로 등재돼 있지만, 그에 상응하는 선형 검색 함수는 표준이 아니어서 그런지 _lfind이다.

3. 스택 메모리의 임의 할당

그러고 보니 비표준 함수 중에는.. malloc의 변종으로서 가변 길이(= 크기가 런타임 때 정해지는) 메모리를 heap이 아닌 무려 현재의 스택 메모리에서 얻어 오는 alloca이던가 malloca인가 하는 물건도 있었다. 옛날 16비트 Turbo C에만 있는 줄 알았는데 현재의 Visual C++에서도 지원은 하는가 보다. 물론 앞에 밑줄은 붙여서 말이다.

얘는 C에서 문법적으로 가능하지 않은 동적 배열을 heap이 아닌 스택 메모리에 구현해 준다. 메모리 할당 속도가 heap을 다루는 것보다 훨씬 더 빠르며, 함수 실행이나 scope이 끝날 때 해제도 자동으로 되어 memory leak 걱정을 할 필요 없으니 편리하다. 지금 실행 중인 함수의 stack frame을 조작하는 물건이니, 겉으로는 함수 호출 같지만 실제로는 컴파일러 인트린식 형태로 구현되지 싶다.

이렇게 생각하면 얘는 장점이 많아 보이지만.. 일단 할당 장소가 장소인 관계로 (1) 수 MB 이상급의 대용량 메모리를 할당할 수 없으며, (2) 할당 방식의 특성상 heap 메모리처럼 할당과 해제를 무순으로 임의로 자유자재로 할 수 없다. (3) C++ 언어의 보조를 받는 게 없기 때문에 해제와 C++ 객체 소멸을 한데 연계할 수도 없다.

이런 한계로 인해 스택에서의 동적 메모리 할당은 생각만치 그렇게 유용하지 않다. 본인도 지난 20여 년 동안 C/C++ 프로그래밍을 하면서 이걸 전혀 사용해 본 적이 전혀에 가깝게 없었다.
저 함수가 괜히 비표준이 아닌 셈이다. 마치 정수 기반 고정소수점과 비슷한 위상의 이단아인 것 같다. 다만, 그럼에도 불구하고 본인은 이런 상황은 어떨까 하는 생각을 해 보았다.

생성자에서 문자열을 인자로 받아들여 적절한 처리를 하는 기반 클래스가 있다.
이걸 상속받아 파생 클래스가 만들어졌는데, 얘는 자주 쓰이는 문자열 패턴을 손쉽게 생성하기 위해 여러 개의 문자열이나 숫자를 숫자를 인자로 받으며, 이로부터 단일 문자열을 생성하여 기반 클래스의 생성자에다가 전달한다. 즉, 이런 꼴이다.

Derived::Derived(string arg1, string arg2, int num):
 Base( prepareArgument(arg1, arg2, num) ) {}

예시를 보이기 위해 편의상 string이라는 자료형을 썼지만, 실제로 저기서 쓰이는 것은 const char * 같은 문자열 포인터이다.
즉, 나는 Derived의 생성자에서 char buf[128] 같은 스택 기반 지역변수 배열을 선언한 뒤, 거기에다 arg1, arg2, num의 정보를 담고 있는 문자열을 담고 그걸 Base의 생성자에다가 전달하고 싶으나.. 문법 구조상 그건 가능하지 않다. 기반 클래스는 파생 클래스의 생성자가 실행되기 전에 초기화가 완료돼야 하기 때문이다. 그러니 파생 클래스의 생성자 함수에서 확보해 놓은 스택 변수의 공간을 받을 방법도 존재하지 않는다.

이럴 때 prepareArgument(_alloca(len), arg1, arg2, num) 이런 식으로 static한 보조 함수를 만들면 굳이 힙 메모리 할당과 생성자· 소멸자가 뒤따르는 범용 string을 쓸 필요 없이 스택에다가 문자열을 담을 공간을 임시로 확보하여 소기의 목적을 달성할 수 있을 것으로 보인다.

4. 쓰레기값

'초기화되지 않은 변수, 쓰레기값'이라는 건 내가 아는 프로그래밍 언어들 중에는 C/C++에만 존재하는 개념이다. 물론 컴퓨터라는 기계에 본질적으로 존재하니까 C/C++에도 존재하는 것이지만 말이다.
이것 때문에 야기되는 버그의 황당함과 막장스러움은 뭐, 이루 말로 형용할 수 없다. 같은 소스 코드가 release 빌드와 debug 빌드의 실행 결과가 달라지는 건 애교 수준이다. 프로그램이 미묘하게 삑사리가 나고 있어서 몇 시간을 끙끙대며 디버깅을 했는데 원인이 고작 이것 때문인 일이 비일비재하다.

개인적으로는 부모 클래스의 멤버가 초기화되지 않았는데 그걸 자식 클래스에서도 초기화하지 않은 것, 처음에는 0 초기화가 보장되는 static 영역에 있던 오브젝트를 별 생각 없이 스택/힙으로 옮긴 것, 심지어 한 멤버를 초기화할 때 아직 초기화되지 않은 다른 멤버를 참조해서 망한 것이 기억에 남아 있다.

간단한 int 지역변수가 초기화되지 않은 건 컴파일러 차원에서 잡아 주지만 위와 같은 사항들, 복잡한 구조체의 멤버가 일부 초기화되지 않는 것, 스택이 아닌 힙에서 할당하는 동적 메모리가 돌아가는 사정은 컴파일러도 일일이 다 챙겨 주지 못하기 때문에 더 복잡한 정적 분석의 영역으로 가야 한다.

그런데 개인적으로 의문이 드는 건 초기화되지 않은 쓰레기값이란 건 어느 정도로 무질서하냐는 것이다. 무슨 수학적으로 균일한 난수 수준일 리는 없을 것이다.
그 쓰레기들의 값에 영향을 주는 것은 정확하게 무엇일까? (스택이냐 힙이냐에 따라 다르게 생각해야 할 듯) 컴파일 시점에서 결정되어서 한번 빌드된 프로그램은 동일한 동작 조건에서는 불변인 걸까? 혹은 운영체제에 따라 달라질 수 있을까?

마치 중간값 피벗 기반의 퀵 정렬이 최고 시간 복잡도가 나오게 공격하는 방법을 연구하는 것처럼 저것도 뭔가 컴퓨터공학적인 고찰이 필요한 의문인 것 같다.

5. 메모리 주소의 align 문제

"어..? 구조체의 크기가 왜 각 구조체 멤버들의 크기의 합보다 더 크지? 컴파일러의 버그인가?"
본인이 이렇게 크게 놀랐던 게 벌써 20여 년 전, 고딩 시절에 도스용 DJGPP를 갖고 놀던 때였다.
그때는 지금 같은 구글 검색도 없고 네이버 지식인도 없고.. 이런 시시콜콜한 이슈를 다루는 C언어 서적도 없었으니, 궁금하면 물어 볼 만한 곳이 PC 통신 프로그래밍 관련 동호회 게시판밖에 없었다.

메모리 취급에 매우 관대한 x86 물에서만 놀던 사람이라면 word align이라는 개념이 더욱 생소할 수밖에 없다. 더구나 그 경계에 맞지 않은 단위로 메모리 접근을 시도할 경우, CPU가 귀찮아서 예외까지 날린다면??
본인은 포팅이라는 걸 할 때 word align을 조심해야 한다는 것을 머리로는 들어서 알았지만 그 문제를 회사에서야 실제로 겪었다.

이제 네이티브 코드는 반드시 ARM64 기반으로 빌드해야 하니 해당 부분을 64비트로 다시 빌드했다. 그런데 동일한 엔진을 얹은 안드로이드 앱이 어떤 기기에서는 잘 돌아갔는데 다른 기기에서는 뻗었다.
죽은 지점이 어딘지는 stack dump를 통해 알아낼 수 있었지만 거기는 null pointer, buffer overflow 등 그 어떤 통상적인 메모리 문제가 발생할 여지도 없는 곳이었다.

알고 보니 거기는 파일 형태로 기록하는 조밀한 버퍼에다 wcsncpy( reinterpret_cast<wchar_t*>(buf+1), str, len) 이런 짓을 하고 있었으며, 타겟 포인터가 한눈에 보기에도 wchar_t의 크기 대비 word align이 되어 있지 않았다(buf 는 char* ㄲㄲㄲ).
그래서 wcsncpy를 memcpy로 교체함으로써 문제를 해결할 수 있었다. wchar_t는 long과 더불어 포팅을 어렵게 하는 주범이며, reinterpret_cast는 align과 관련된 잠재적 위험성을 발견하는 용도로도 쓰일 수 있다는 점을 알게 됐다.

복잡한 포인터 메모리 조작 코드에서 잠재적인 align 문제를 잡아내는 건 사람의 디버깅만으로는 한계가 있을 텐데, 정적 분석으로 가능한지 궁금하다. 그리고 반대로 레거시 코드를 돌리기 위해 컴파일러가 성능이 떨어지는 걸 감수하고라도 최소한 뻗지는 않게 align 보정 코드를 집어넣어 주는 옵션도 있을 텐데 그것도 궁금해진다.

6. 32비트 단위 문자열

C/C++에서 wchar_t 크기의 파편화로 인해 야기된 혼란과 원성이 워낙 장난이 아니었기 때문에 C++11에서는 아예 크기를 직접 명시하고 고정시킨 char16_t와 char32_t라는 자료형이 built-in으로 추가되었다. int는 32비트 시대에 크기가 변했고 long은 64비트 시대에 플랫폼별로 삐걱거리기 시작했다면, wchar_t는 유니코드와 함께 새로 등장하면서 저 지경이 된 셈이다.

개인적으로 인상적인 것은 char32_t는 U""라고 문자열 리터럴을 나타내는 접두사까지 언어 차원에서 새로 등장했다는 점이다. 드디어 확장 평면 문자도 취급하기 더 수월해지겠다.
그런데 그러면 Visual C++이라면 ""는 1바이트, L""는 2바이트, U""는 4바이트라고 자연스럽게 연결되는데, 처음부터 wchar_t가 4바이트였던 맥에서는 L과 U가 모두 4바이트이다. char16_t에 대응하는 2바이트 문자열은 리터럴로 표현하는 방법이 없나 궁금하다. 오히려 Objective C에서 사용하는 NSString의 @""가 2바이트 문자열 리터럴이다.

char32_t가 언어 차원에서 이렇게 지원되기 시작했는데 str*, wcs*처럼 32비트 문자열 버전에 대응하는 strlen, strcpy, sprintf 등도 있어야 하지 않나 싶다. C++이라면 char_traits 템플릿으로 땜빵할 수도 있겠지만 C에는 그런 게 없으니까..
그리고 템플릿이 없는 저쪽 동네 Java는 32비트 단위 문자열을 취급하는 string class 같은 것도 있어야 하지 않을까? 문자 하나도 확장 평면까지 감안해서 얄짤없이 int로 표기하는 건 직관적이지 못하고 불편하니 말이다.

7. 레퍼런스 사이트

C/C++은 마소의 C#, 애플의 Swift, 썬-오라클의 Java처럼 한 기업이 주도해서 개발하는 언어가 아니다. 그래서 C++ 라이브러리 레퍼런스 같은 걸 검색해도 딱 떨어지는 개발사의 홈페이지가 곧장 나오지는 않는다.
하지만 수 년 전부터 구글 검색에서 상위권으로 노출되고 있는 유명한 사이트는 아래의 딱 두 곳인 것 같다.

http://www.cplusplus.com
https://en.cppreference.com/w

얘들은 개인? 단체? 어디서 운영하는지 모르겠다. C++17, C++20 같은 최신 정보도 곧장 올라오는 걸 보니 유지보수도 활발히 되고 있고 만만하게 볼 퀄리티가 아니다.
마치 Doom 게임 관련 자료를 듬뿍 얻을 수 있는 위키 사이트가 doomwiki와 doom.fandom.com 요 두 계열로 나뉘듯이 말이다.

Posted by 사무엘

2019/11/13 08:33 2019/11/13 08:33
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1683

Trackback URL : http://moogi.new21.org/tc/trackback/1683

Leave a comment
« Previous : 1 : ... 7 : 8 : 9 : 10 : 11 : 12 : 13 : 14 : 15 : ... 1571 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/12   »
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        

Site Stats

Total hits:
1295898
Today:
291
Yesterday:
461