레이블이 pointer인 게시물을 표시합니다. 모든 게시물 표시
레이블이 pointer인 게시물을 표시합니다. 모든 게시물 표시

2018-03-04

2018년 9번째 주

 이 포스팅은 그냥 지난 한 주간 읽었던 것들을 정리하는 포스트입니다. 그냥 예전에 봤던 글 중 나중에 필요한데 뭐였는지 기억 안 나는 글들이 있어서 쓰기 시작했습니다.
 보통 하는 일과 관련된 글들이 올라오겠지만 딱히 정해둔 주제는 없고, 그때그때 관심 있었던 것을 읽었기 때문에 지난주에 쓰인 글일 수도 있고 몇 년 전에 쓰인 글일 수도 있습니다.

Lifetime Safety: Preventing Leaks and Dangling

 Herb Sutter와 Neil MacIntosh가 쓴 C++에서 memory leakdangling pointer를 어떻게 없앨 수 있는지에 관한 글이다. 일반적으로 C++의 포인터는 매우 강력하기 때문에, memory leak이나 dangling pointer를 없애기 위해서 C++의 기능을 일부 제한하거나 새로운 문법을 추가하거나 한다. 하지만 이 글에서는 언어를 바꾸지 않으면서 런타임 오버헤드 없이 컴파일 타임에 분석할 수 있는 알고리즘을 제시한다.
 특히 이 알고리즘은 프로그램 전체를 분석하는 것이 아니라 함수 단위, 정확히는 블록 단위로 적용할 수 있고, 변수 재사용 등 많은 스타일 가이드에서 권하지 않지만 실제로는 많이 사용되는 패턴들에 대해서도 고려돼있기 때문에 레거시 코드에 적용하기도 좋다.
 기계적인 작업이기 때문에 툴을 만드는 것이 가장 좋을 것이다. 하지만 툴을 만들 여유가 없더라도 포인터나 레퍼런스를 어떻게 써야 안전한지 보여주는 좋은 글이기 때문에 일단 읽어보는 것을 추천한다.

GitHub DDoS 공격당함

 지난 2018년 2월 28일, GitHubDistributed Denial-of-Service(a.k.a. DDoS) 공격을 당해 약 10분 정도 서비스가 멈췄었다. 이 공격은 memcached를 이용한 공격으로 중국의 0kee team이 찾은 Deluge라는 기법을 이용한 공격이었다.
 Deluge는 다른 서버에 설치된 memcached에 데이터를 요청할 때 source IP를 목표가 되는 서버의 IP로 수정하여 보내, memcached 서버가 목표가 된 서버로 데이터를 보내게 만드는 공격이다.
 이런 부류의 공격을 IP spoofing이라고 하는데, 이는 Internet protocol(a.k.a. IP)OSI 7 layer에서 말하는 session layer에 관한 부분이 없기 때문에 생기는 근본적인 취약점을 이용한 것이기 때문에, 네트워크 프로토콜을 IP 위에 올린다면, 프로토콜을 작성하는 차원에서 세션 처리를 신경 쓰지 않았다면 근본적으로 막을 방법은 없다.
 IP spoofing 자체는 흔하게 사용되는 공격법이지만 memcached를 사용하는 Deluge는 그중에서도 큰 획을 그었다. 단순히 GitHub을 공격했기 때문이 아니라 적은 패킷으로 많은 데이터를 공격하게 할 수 있기 때문이다. 이 비율을 bandwidth amplification factor라고 하는데 Deluge는 지금까지 알려진 IP spoofing 공격 중에서 가장 높다.

2018-02-09

[C] tagged pointer - 포인터에 정보 담기

 Tagged pointer는 메모리 크기를 줄이기 위한 고전적인 테크닉이다. 기본적인 아이디어는 포인터의 모든 값이 의미 있는 값은 아니라는 것이다. 예를 들어 4 byte 단위로 align 되는 객체의 32-bit 포인터를 생각해보자. 그렇다면 이 객체의 주소는 4로 나누어 떨어지는 값이 돼야 하니 LSB(Least Significant Bit)으로 부터 2 bit은 언제나 0b00으로 고정될 것이다. 그렇다면 이 2 bit을 다른 정보를 담는 데 써도 아무 문제가 없다. 조금 더 구체적으로 경우 포인터의 값이 0x5678FFF0, 0x5678FFF1, 0x5678FFF2, 0x5678FFF3인 경우 모두 0x5678FFF0에 있는 객체를 가리키도록 하고, 0x5678FFF4, 0x5678FFF5, 0x5678FFF6, 0x5678FFF7인 경우 모두 0x5678FFF4를 가리키는 포인터로 해석하는 것이다.

 Tagged pointer를 만드는데 LSB 만 쓸 수 있는 건 아니다. 보통 user space에서 쓸 수 있는 최대 메모리가 제한돼 있다. 예를 들어 32-bit 윈도우에서 user space는 최대 3GB 까지 늘릴 수 있지만, 기본적으로 2GB이다. 즉, MSB(Most Significant Bit) 1 bit를 tag에 쓸 수 있다. 64-bit 리눅스라면, 프로세스당 최대 메모리 스페이스는 256 TB까지 이므로 48 bit만 사용된다. 즉, MSB로부터 16 bit를 tag에 사용할 수 있다. 하지만 위의 두 예시에서 보았듯이 tag에 이용할 수 있는 MSB의 크기는 시스템별로 다르다. 따라서 MSB를 tagged pointer로 사용하는 경우 portable 한 코드를 만들기 어려워진다.

 Tagged pointer를 모든 포인터에 일반적으로 적용하지 않아도 된다. 그보다는 테이블같은 것에 저장할 포인터에만 사용하거나 포인터를 리턴하는 함수에 대해서만 사용하는 것이 일반적이다. 특히 포인터와 추가 정보를 리턴하는 함수를 tagged pointer를 리턴하는 함수로 만들면, 리턴값이 하나의 레지스터에 들어가게 돼서 더 빨라지기도 한다.

 Tagged pointer는 사용하기 불편한 테크닉이지만 놀랍게도 아직 종종 사용된다. 하지만 사용하기 전에 정말로 필요한지 고민해봐야 한다. Tagged pointer는 사용하는 메모리 크기를 줄이지만, 구현이 쓸데없이 복잡해진다. 구현이 복잡하다는 것은 버그가 발생할 확률이 높다는 것이고, 게다가 디버거는 tagged pointer의 의미를 알지 못 하므로 디버깅도 많이 귀찮아진다. 이 개발 비용을 감수하면서도 tagged pointer가 주는 이득이 충분한지 따져보고 사용해야 한다.

2018-02-07

[CppCoreGuidelines] 포인터 구분해서 쓰기 - span, owner

 C++을 쓰는 사람들이 가장 어려워하는 것 중 하나가 포인터다. 그중에서도 함수 포인터를 읽고 해석하는 것이 가장 어렵다고 한다. 하지만 실제 코드에서는 함수 포인터를 볼 일은 거의 없다. 특히 modern c++에서는 가능하면 std::function를 쓰는 걸 권장하기 때문에 몇몇 특수한 목적을 가진 코드를 제외하고는 함수 포인터를 볼 일은 거의 없다.

 그다음으로 어려운 것은 메모리 관리다. C++에서 전통적으로 많이 발생하던 문제가 double free와 memory leak이다. 이는 C++에서 포인터로 가리키는 객체의 소유권이 명확하지 않기 때문이었다. 이를 해결하기 위해 C++11에서는 소유권을 혼자 차지하고 있는 std::unique_ptr과 소유권을 공유하는 std::shared_ptr을 만들었다. std::unique_ptrstd::shared_ptr을 잘 활용하면 dobule free와 memory leak은 예방할 수 있다.

 하지만 C++11 이후에도 여전히 포인터는 다양한 역할을 가지고 있다. 현재 C++의 포인터에 남은 역할은 다음과 같다.

  1. std::unique_ptr을 사용하지 않지만, 소유권을 넘길 때
  2. 함수의 인자로 배열을 넘길 때
  3. 문자열을 가리킬 때
  4. 소유권을 넘기지 않고 하나의 객체를 가리킬 때

스마트 포인터를 사용하지 않지만, 소유권을 넘길 때

 앞에서 말했듯이 C++11은 std::unique_ptrstd::shared_ptr를 도입하여 소유권을 관리할 수 있도록 하였다. 하지만 스마트 포인터를 사용하지 못하는 경우도 있다. 이 경우 적절한 지점에서 객체를 소멸시켜줘야 한다. 하지만 이를 지칭하는 것이 단순히 포인터이기 때문에, 메모리를 소멸시켰는지, 한 번만 소멸시켰는지 알기 어렵다. 그래서 C++ Core Guidelines에서는 이 경우 owner<T>라는 클래스를 사용하는 것을 권장한다. owner<T> 클래스는 아무런 일도 하지 않는 클래스다. 사실 클래스일 필요도 없기에 GSL에서는 포인터의 alias로 구현하고 있다.
 owner<T> 자체는 아무것도 하지 않지만, 포인터와는 구분되기 때문에 코드를 읽는 사람이 소유권을 명확히 알 수 있다는 장점이 생긴다. owner<T>를 인자로 받은 함수가 이 값의 소유권을 넘기지 않았다면 이 함수에서 객체를 소멸시킬 책임을 진다. 다시 말해 이 값을 다른 함수에 넘기거나, 새 스마트 포인터를 만들거나, 리턴하거나, 전역 변수에 할당한 것이 아니라면, 이 함수에서 소멸시켜야 한다.

함수의 인자로 배열을 넘길 때

 C++ 11에는 std::array가 들어왔지만, 여전히 배열을 함수의 인자로 사용할 때는 첫 번째 원소의 포인터와 배열의 길이를 따로 넘기는 방법이 많이 사용된다.
 이유는 크게 2가지 있다. 일단 아래와 같이 배열이나 std::array를 쓰면, 이 함수는 배열의 길이별로 전부 다른 함수로 instantiation 된다.  또한, 이 함수는 컴파일 타임에 크기가 정해지는 배열에 대해서만 사용할 수 있으므로, 동적 크기 배열에 대해서는 함수를 처음의 방식으로 재정의해야 한다.
 이런 문제를 해결하기 위해 C++ Core Guidelines에서는 span<T>이라는 클래스를 정의해서 사용하기를 권장한다. span의 생성자가 배열, std::array, 동적 크기의 배열과 길이를 받는 생성자를 전부 정의하고 있으면, 배열을 받아야 하는 함수는 아래와 같이 span<T>을 이용하면 된다.  span의 여러 가지 장점을 가진다. 우선, 동적 배열과 정적 배열 양쪽에 모두 사용할 수 있다. 또한, 포인터와 길이를 묶기 때문에, 포인터의 길이를 잃어버릴 걱정도 안 해도 된다. 마지막으로 포인터가 지칭하고 있는 것이 배열의 첫 번째 원소인지 단일 객체인지 고민하지 않아도 된다. 실제로 C++ Core Guidelines를 따르는 코드에서는 포인터가 보이면 무조건 단일 객체라고 생각해도 된다.

C-style의 문자열을 가리킬 때

 C++에서 포인터를 사용하는 또 다른 용도는 null 캐릭터로 끝나는 C 스타일 문자열을 지칭할 때다. 이 경우 zstring을 이용하면, 이것이 단일 캐릭터인지, 길이를 가지는 문자열인지, null 캐릭터로 끝나는 문자열인지 고민하지 않아도 된다.
 C 스타일의 문자열은 zstring을 이용하면 되지만, non-ascii 캐릭터를 사용할 경우 null 캐릭터가 문자열의 마지막을 의미하지 않을 수도 있다. 이 경우 문자열의 길이가 필요하다. 이런 종류의 문자열을 명시하기 위해서 위에서 설명한 span<T>을 사용할 수도 있다. 하지만 C++ Core Guidelines에서는 문자열이라는 것을 더욱 명확히 명시하기 위해서 string_span라는 이름을 사용할 것을 권장한다.

소유권을 넘기지 않고 하나의 객체를 가리킬 때

 사실 일반적인 C++ 코드에서 볼 수 있는 포인터는 대부분 소유권을 가지지 않는 하나의 객체를 가리크는 경우이다. 그리고 C++ Core Guidelines를 완벽하게 따르는 코드에서는 포인터를 사용하는 것을 사용하는 유일한 경우다. 따라서 C++ Core Guidelines를 잘 따르는 코드에서 포인터가 보이면 이는 소유권을 가지지 않는 단일 객체를 지칭하는 것으로 생각할 수 있다.

 이상으로 C++ Core Guidelines에서 포인터를 어떻게 구분해서 사용하는지 보았다. 보면 알겠지만, owner<T>zstring은 타입에 새 이름을 붙인 것이고, span<T>string_span은 관련 있는 데이터를 하나의 구조체로 묶은 것이다. 매우 기초적이고 단순한 것이지만, 무시해서는 안 된다. 실제로 위의 네 타입만 잘 써도 포인터를 사용하면서 발생하는 많은 문제을 쉽게 해결, 혹은 발견할 수 있다.