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

2018-08-09

[Rust] _는 bind하지 않는다

RustRAII idiom을 사용하는 언어로, 객체가 소멸하는 시점에 따라 코드의 의미가 달라진다. 예를 들어 아래 코드를 보자.

이 코드는 Service의 객체를 생성하고 종료하기를 기다리는 코드다. 이 코드는 문법적으로는 아무 문제가 없다. 하지만 종료할 때까지 Service가 어떤 동작을 수행하기를 원했다면 이는 틀린 코드다. Service 객체는 아무 변수에도 bind 되지 않았기 때문에 이 객체는 두 번째 줄에 있는 문장이 끝나면 소멸한다.

Service 객체가 wait_for_exit이 수행될 때까지 살아있기를 원한다면, 아래와 같이 변경해야 한다. 위의 코드에서 Service의 객체는 변수 service에 bind 된다. 따라서 두 번째 라인이 끝나도 소멸하지 않고 wait_for_exit이 종료되는 것을 기다리고, run 함수가 종료되면서 stack이 unwind 될 때 소멸한다.

하지만 위의 코드를 컴파일하면 server가 unused variable이라는 경고가 보일 것이다. Rust 컴파일러는 선언된 변수가 사용되지 않으면 경고를 내기 때문이다. 그렇다면 사용하지 않는 변수의 소유권만 가지고 있고 싶을 때는 어떻게 해야 할까? 이 경우 _(underscore)로 시작하는 변수 이름을 사용하면 된다. Rust 컴파일러는 _로 시작하는 변수를 특별 취급하기 때문에, _로 시작하는 변수는 사용하지 않아도 컴파일러 경고가 나지 않는다.

그렇다면 사용하지 않는 변수에 아무런 이름을 주지 않으면 어떻게 될까? 어차피 사용하는 것이 목적이 아니고, 객체를 bind만 해서 가지고 있는 것이 목적이라면 아래 코드처럼 아무 이름 없이 _를 변수로 사용해도 되지 않을까?

아쉽게도 위 코드는 예상대로 동작하지 않는다. 이는 _가 객체의 소유권을 가지지 않기 때문이다. 이를 Rust의 용어로는 _는 value를 bind 하지 않는다고 말한다. 즉, 위의 코드는 Service 객체를 생성하고 소멸시킨 뒤 wait_for_exit 함수를 실행하는 service_run1.rs와 같은 코드다.

사실 이는 처음부터 의도된 동작은 아니었다. 2013년에 _에 넣은 값이 바로 drop 된다는 것이 버그로 보고됐고, _를 일반적인 변수와 같이 bind 하도록 고치자는 제안도 있었다. 사실, bind 하는 것이 더 직관적이고 일반적인 접근 방법이다. 하지만, Rust 개발자들은 _의 의미를 bind 하지 않는 것으로 스펙에 규정했다. 이는 어떤 값을 받아서 bind 하지 않는 방법이 기존에 존재하지 않았기 때문이다.

값을 bind 하지 않는 _의 특성은 다음과 같은 경우에 유용하게 사용된다. 위 코드는 어떤 tuple을 받아서 Option<A>를 먼저 사용하고, 그 뒤 B를 사용한다. 만약 _가 값을 bind 하였다면, 네 번째 줄과 다섯 번째 줄에서 B가 move 되었을 것이다. 따라서 여덟 번째 줄은 이미 move 된 값을 사용하였다는 경고와 함께 컴파일되지 않았을 것이다. 하지만 _는 값을 bind 하지 않기 때문에 위의 코드는 성공적으로 컴파일된다.

여기서 _로 받은 값을 statement가 끝난 뒤 drop 하는 것이 아닌 값을 bind 하지 않는 것으로 정의했다는 것이 중요하다. 위의 service_run4.rs 예제에서 Service 객체가 drop 된 것은 temporary value를 bind 하지 않았기 때문이지 _가 값을 drop 시키기 때문이 아니다. 만약 _가 값을 drop 한다면, non_bind.rs 예제에서 match 문이 끝난 뒤 B가 drop 되기 때문에 여덟 번째 줄에서 B를 사용할 수 없었을 것이다.

개인적으로 이것 때문에 발생한 버그가 Rust를 사용하면서 겪었던 버그 중 해결에 가장 오랜 시간이 걸린 버그였다. 현재 개발 중인 CodeChain은 멀티스레드 프로그램이기 때문에 main thread에서 여러 개의 서비스를 생성하고 각 서비스는 종료될 때까지 자신이 해야 할 일을 한다. 이때 대부분 서비스는 자기 완결적인 일을 하므로 외부에서 사용하지 않고 객체를 유지하기만 하면 된다. 따라서 변수의 이름을 _로 사용해도, 컴파일러 입장에서는 틀린 코드가 아니기 때문에 성공적으로 컴파일된다. 게다가 다른 언어에서는 _를 변수로 사용하는 것이 아무 문제 없기 때문에 코드를 읽어도 의심 없이 넘어갔었다.

이런 부류의 버그는 모르고 보면 잡기 어려운 문제지만, 알면 매우 쉬운 문제다. 하지만 Rust 공식 문서인 “The Rust Programming Language”에서도 “However, using the underscore by itself doesn’t ever bind to the value. Listing 18-22 will compile without any errors because s doesn’t get moved into _.”라고 단 두 줄 나오고 넘어가기 때문에 Rust의 _는 값을 bind 하지 않는다는 것을 모르기 쉽다. 그래서 앞으로 Rust를 쓰는 사람이 같은 버그를 만나지 않았으면 하는 마음으로 이 글을 쓴다.

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 공격 중에서 가장 높다.

2017-07-24

[C++] object는 언제 생성돼서 언제 소멸되는가 - storage

 C++에서는 타입을 가지는 일정 크기의 값을 object라고 한다. object의 중요한 특징 중 하나는 lifetime. 즉, object가 언제 소멸하는가 하는 것이다. 이 object는 객체 지향 프로그래밍에서 말하는 객체와는 살짝 다른 개념이다. C++ 표준에서 object는 타입, alignment, lifetime 등의 특성을 가지는 일정 크기의 연속된 값을 의미한다. 이는 객체 지향 프로그래밍에서 말하는 객체와는 약간 다른 의미이지만 이 글에서는 그냥 객체라고 부르도록 하겠다.
 C++에서 객체의 lifetime을 알기 위해서는 우선 객체가 어디에 생성되는지를 알아야 한다. 일반적으로 C++ 개발자에게 객체가 어디에 생성되는지를 물으면 다음과 같이 대답할 것이다.

 로컬 객체는 stack에, new로 생성된 객체는 heap에 저장되는데 stack과 heap은 같은 메모리 영역을 공유하며 stack은 메모리가 감소하는 방향으로 커지고, heap은 메모리가 증가하는 방향으로 커진다. 또한, 전역 변수 중 initialize 되지 않은 것은 bss에 initialize 된 것은 data 섹션에 저장된다.
 이 대답은 기술적으로 맞는 대답이다. 사실 매우 훌륭한 대답이다. 실질적으로 대부분의 머신에서 대부분의 컴파일러는 위와 같은 방식으로 코드를 컴파일한다. 하지만 다시 한번 생각해보자. stack과 heap이 같은 메모리를 나누어 쓰는 이유는 한정된 크기의 메모리에서 컴파일 타임에 알 수 없는 두 종류의 동적 메모리를 할당하기 때문이고, bss 영역과 data 영역이 나누어지는 이유는 바이너리 파일의 크기를 줄이기 위한 최적화 때문이다. 즉, stack, heap, data, bss 등의 메모리 영역 등은 구현에 관련된 것일 뿐이다. 그렇다면 C++ 표준 문서에서는 이에 대해서 어떻게 기술하고 있을까?
 C++ 표준에서는 객체가 메모리에 생성된다고 하지 않고, 스토리지에 생성된다고 한다. 구현과 표준을 분리하기 위한 결정으로 구체적인 메모리 레이아웃 등을 정하지 않음으로써 컴파일러들에게 실행 머신에 따라 최적화시킬 여지를 주기 위해서다. C++에는 원래 automatic, dynamic, static 3개의 스토리지가 있었고 C++11에서 thread_local이 추가되어 네 종류의 스토리지가 있다.

automatic storage

 가장 기본적인(?) 스토리지는 automatic storage다. C++에 익숙한 사람이라면 스택 영역이라고 생각하면 된다. static specifier가 없는 로컬 객체는 automatic storage에 할당되는데, 이는 선언된 위치에서 생성되고, block을 떠날 때 생성된 역순으로 소멸된다.
 C++ 11 이전에 있었던 auto specifier는 객체를 automatic storage에 저장하라는 의미였다. 로컬 객체는 따른 specifier가 없으면 모두 automatic storage에 저장되므로 아무 의미가 없으므로 별로 사용되지 않았고 지금은 auto 키워드는 타입 추론과 관련된 의미로 변경됐다.

dynamic storage

 dynamic storage는 다른 이름으로 free storage라고 부르기도 한다. new 키워드로 생성된 객체는 dynamic storage에 생성되고 명시적으로 delete가 불리기 전에는 소멸하지 않는다. 일반적으로 dynamic storage에 저장되는 object는 메모리의 힙 영역에 저장되도록 구현된다. delete가 불리지 않으면 소멸하지 않고, 같은 객체에 대해서 두 번 호출되면 메모리가 오염되는 문제가 생긴다. 따라서 dynamic storage에 생성되는 객체를 automatic storage에 생성되는 객체와 같은 라이프타임을 가지게 하는 RAII같은 기술이 많이 사용된다. 특히 C++11 이후의 modern C++에서는 shared_ptr이나 unique_ptr에 종속되지 않은 dynamic 객체가 존재하는 것을 좋지 않은 코드로 본다.

static storage

 구현에서는 초기화됐는지 아닌지에 따라 data 영역과 bss 영역을 구분한다. 하지만 이는 바이너리 파일의 크기를 줄이기 위한 최적화일 뿐이고 표준 문서에서는 둘을 구분하지 않는다. 명시적으로 static 한정자가 붙었거나, namespace 스코프의 변수는 모두 static storage에 생성된다. 초기화됐건 선언만 했건 둘 다 static storage에 할당되고 그저 초기화되지 않은 static 객체는 0으로 초기화된다고 할 뿐이다.
 static storage에 생성된 객체는 프로그램이 종료될 때, 생성이 완료된 순서의 역순으로 소멸한다. 하지만 프로그램이 어떻게 종료되더라도 언제나 소멸되는 것은 아니다. main 함수가 종료돼서 끝나거나 exit 함수에 의해서 종료되면 static 객체는 소멸하지만 termiate에 의해서 종료되거나 예외를 잡지 못해서 종료되면 static 객체는 소멸하지 않는다.

thread-local storage

 thread-local storage는 C++11이 스레드를 지원하면서 새로 생긴 스토리지다. 변수 선언 시 thread_local 한정자를 붙이면 thread-local storage에 객체가 할당된다. 이는 static storage와 비슷하지만 스레드별로 생성되고 그 라이프타임도 스레드에 종속된다
 보통의 컴파일러들이 thread local 객체를 구현하는 방법도 static 객체와 비슷하다. 선언만 된 객체들의 크기의 합과 초기화된 객체의 값이 따로 저장된다. 각 영역은 흔히 tbss와 tdata라고 불린다. 파일 포맷에서는 static object와 비슷하지만, 실행시킬 때는 사실 stack에 저장되기 때문에 메모리상에서는 스택과 비슷한 위치에 생성된다. 조금 더 정확히는 스레드별로 별개의 스택 시작 위치를 가지는데, 이때 스택의 시작 부분에 thread local 객체들을 생성한다.
 thread local 객체는 스레드 시작에서 생성자가 불리고, 스레드가 종료될 때 소멸하는데 static 객체와 마찬가지로 예외에 의해서 종료되거나 terminate에 의해서 종료되면 thread local 객체는 소멸하지 않는다.

 이상이 C++11이 가지고 있는 네 종류의 스토리지에 대한 설명이었다. 이 스트로지는 객체의 lifetime을 결정하기 때문에 잘 알고 있어야 한다. automatic storage에 할당된 객체는 그 레퍼런스가 해당 block을 벗어나지 않도록 해야 하고, dynamic storage에 할당된 객체는 반드시 한 번만 소멸하도록 신경 써서 코드를 작성해야 하며, static storage는 프로그램 전체에서 하나만 생기고, thread-local sotrage는 스레드마다 하나씩 생기며 스레드와 라이프타임을 공유한다. static storage와 thread-local storage에 생성된 객체의 소멸자는 생성자가 완료된 순서의 역순으로 호출된다는 것을 조심해야 한다. 소멸한 객체에 대한 접근은 잡기 힘든 버그를 만드니 자신이 만든 객체가 어떤 storage에 저장되고 어떤 lifetime을 가지는지 반드시 신경 써야 한다.