[Valgrind] Details of Memcheck

valgrind Memcheck 기능의 원리


4.5.1. Valid-value (V) bits

Memcheck는 실제 CPU와 동일한 synthetic CPU를 구현한 것이라고 생각할 수 있다. 단, synthetic CPU는 실제 CPU와 다른 점이 하나 있다. 실제 CPU에 의해 처리되는 모든 bit들은 synthetic CPU에서 연관된 "valid-value" bit를 가진다. 이 비트들은 실제 CPU에 존재하는 연관된 비트가 유효한(legitimate) 값을 가지는지를 나타낸다. 이런 synthetic CPU의 비트를 V(valid-value) bit라고 한다.

valgrind memcheck의 대상이 되는 시스템의 모든 바이트는 항상 8개의 V bit를 가진다. 예를 들어 CPU가 32바이트를 메모리에서 로드하면, 32 V bits도 로드한다. CPU가 값의 일부 또는 전체를 나중에 메모리의 다른 위치에 기록해야 할 경우, 대응되는 V 비트들이 V-bit 비트맵(메모리 공간 전체에 대응되는 V 비트들을 저장하는 공간)에 저장된다.

요약하면, 프로그램의(the system) 모든 비트는 개념적으로 연관된 V 비트를 가진다. 이 V 비트는 항상 실제 CPU의 비트를 따라다닌다. 모든 CPU의 레지스터도 대응되는 V bit vector를 가진다. Memcheck은 V 비트 정보를 매우 압축된 형태로 저장하여 관리한다.

Memcheck는 단순한 값의 복사를 잡는 것이 아니다. 값이 프로그램의 외부적으로 보이는 동작에 영향을 미칠 가능성이 있는 방식으로 사용될 경우, Memcheck는 관련 V비트를 즉시 확인한다. 이때, 만약 값이 일부라도 정의되지 않은 상태라면 오류가 보고된다.

int i, j;
int a[10], b[10];
for ( i = 0; i < 10; i++ ) {
  j = a[i];
  b[i] = j;
}

예를 들어 위와 같이 명백히 의미가 없는 코드에 대해서는 그냥 넘어간다. a의 초기화되지 않은 값들을 b로 복사하는 작업은 프로그램의 동작에 영향을 미치지 않기 때문이다. 하지만 반복문이 아래와 같이 변한다면

for ( i = 0; i < 10; i++ ) {
  j += a[i];
}
if ( j == 77 ) 
  printf("hello there\n");

Memcheck는 초기화되지 않은 변수에 의존하는 조건문 if 에서 지적할 것이다. j += a[i]; 에서는 그냥 넘어간다. 왜냐하면 그 시점에는 정의되지 않은 값이 "눈에 띄지(observable)" 않기 때문이다. Memcheck가 지적하는 시점은 프로그램의 오직 눈에 띄는 action이 결정되는 경우이다.

값이 정의되었는지 아닌지에 대한 검사(Checks on definedness)는 오직 세 부분에서 실행된다. * 값이 메모리 접근에 사용된 경우 * control flow 결정(if, case같은 분기)에 사용된 경우 * system call의 인자로 전달된 경우

만약 검사 과정에서 값이 정의되지 않았음을 감지하면, 오류 메세지가 출력된다. 이후 해당 값은 정의된 것으로 간주한다. 그렇지 않으면 동일한 오류가 계속 보고될 수 있기 때문이다. 즉, Memcheck가 한 번 에러를 보고하지 않은 값에 의해 파생되는 다른 문제는 보고하지 않으려고 한다.

왜 메모리에서 값을 읽는 모든 연산을 검사해서 정의되지 않은 값이 CPU에 로드될 때 경고를 출력하지 않고 굳이 이렇게 복잡한 검사를 하는걸까? 그 이유는 그렇게 하면 제대로 동작하지 않기 때문이다.(완전히 정상적인 C 프로그램에서도 초기화되지 않은 값을 메모리에서 가져오는 경우가 많다) 예를 들어 다음과 같은 구조체를 생각해보자.

struct S { int x; char c; };
struct S s1, s2;
s1.x = 42;
s1.c = 'z';
s2 = s1;

struct S의 크기는 몇 바이트일까? int가 4바이트, char가 1바이트니까 5바이트일까? 틀렸다. 장난감 수준이 아닌 알만한 컴파일러들은 구조체 S의 크기를 word 크기로 올림한다. 이렇게 하지 않으면 일부 CPU에서 struct S 배열을 접근할때 매우 비효율적인 코드를 생성해야 하기 때문이다.

CPU word

특정 컴퓨터 구조(CPU)에서 사용하는 고정된 최소 데이터 단위의 크기. CPU는 메모리에서 word보다 작은 크기의 데이터를 가져오거나 쓸 수 없다. 또한 machine instruction들의 크기는 일반적으로 architecture의 word 크기와 동일하다.

그래서 s1은 메모리 공간 8바이트를 사용하지만 실제로 초기화되는 공간은 5바이트이다. s2 = s1 에서 GCC는 그 의미에 상관없이 8바이트를 복사한다.

Memcheck는 shared memory에 대해서도 V 비트를 관리한다. 다만 shared memory의 경우 하나의 shared memory에 대해 여러 벌의 V 비트들이 존재할 수 있다. 여러 벌의 V 비트들은 프로세스마다 하나씩 가질 수도 있고 프로세스 하나가 여러 벌의 V 비트를 가질 수도 있다. (예컨데 하나의 프로세스에서 하나의 page에 대한 read-only 맵핑과 read-write 맵핑을 따로 가지고 있을 수 있기 때문) 이렇게 여러 벌의 맵핑을 가진 상황에서 Memcheck는 각 맵핑에 대한 V 비트를 각각 관리한다. 이는 false positive 에러를 일으킬 수 있다. 첫 번째 맵핑에서 초기화하고 두 번째 맵핑에서 그 값에 접근하는 경우가 그렇다. 두 번째 맵핑 에 대한 V 비트들은 첫 번째 맵핑에서 공유 메모리의 값을 바꾼 것을 모르기 때문이다. 이런 false positive 에러를 우회하는 방법은 VALGRIND_MAKE_MEM_DEFINEDVALGRIND_MAKE_MEM_UNDEFINED를 사용하는 것이다. (이 매크로를 client request라고 한다)

4.5.2. Valid-address (A) bits

4.5.1절의 내용은 값들의 유효성이 어떻게 설정되고 유지되는지에 대한 설명이다. 프로그램이 특정 메모리 주소에 접근할 수 있는 권한이 있는지 없는지에 대해서는 묻지 않았다. 이번 절에서는 후자에 대해 설명한다.

CPU가 아닌 메모리에 존재하는 모든 byte들에 대해서는 연관된 valid-address (A) bit가 존재한다. 이 비트들은 프로그램이 해당 위치의 값을 읽거나 쓸 수 있는지에 대해 표현한다. 이 A 비트는 값의 유효성에 대해서는 아무 것도 표현하지 않는다. 그것은 V 비트의 역할이다. (V 비트는 메모리 뿐만 아니라 CPU에 저장된 값들에 대해서도 유효성을 표현한다)

검사 대상 프로그램이 메모리에 접근(read/write)할 때마다, Memcheck는 해당 주소의 A bit를 확인한다. 만약 유효하지 않은 주소를 가리키는 비트가 발견되면, 에러가 발생한다. 읽기와 쓰기는 A 비트를 바꾸지 않고 참조하기만 한다.

A 비트들이 설정되고 0으로 설정되는 과정은 다음과 같다. * 프로그램이 실행될때, 모든 전역 변수 영역이 accessible로 표시된다. * malloc 또는 new를 호출할 때, 새로 할당된 영역에 대한 A 비트들이 accessible로 표시된다. 해제될 때에는 A 비트가 inaccessible로 표시된다. * 스택 포인터 레지스터(sp) 값이 증가하거나 감소할 때, A 비트가 설정된다. sp부터 스택의 base 까지 모두 accessible로 표시되고 sp 아래는 inaccessible로 표시된다. * 시스템 콜이 실행되는 동안, A 비트들의 값이 적절히 변한다. 예를 들어 mmap이 성공하면 A 비트들의 값이 변경된다. * client request를 사용해서 A 비트의 값을 명시적으로 변경할 수도 있다.

4.5.7. Client Requests

client request들은 memcheck.h에 정의되어 있다.

  • VALGRIND_MAKE_MEM_NOACCESS - 일정 범위의 주소에 inaccessable 표시한다.
  • VALGRIND_MAKE_MEM_UNDEFINED - 일정 범위의 주소를 accessible하지만 undefined 값이 저장됐다고 표시한다.
  • VALGRIND_MAKE_MEM_DEFINED - 일정 범위의 주소를 accessible하고 defined 값이 저장됐다고 표시한다.

위의 client request들은 Valgrind와 함께 실행될때 -1, 그렇지 않은 경우에는 0을 반환한다.

references