vDSO의 기본 개념을 살펴보고, glibc를 수정하지 않고 리눅스 커널에 사용자 정의 vDSO를 추가한 뒤 이를 사용자 공간에서 사용하는 방법을 단계별로 설명한다.
A vDSO(virtual dynamic shared object)는 GNU/Linux 커널이 제공하는 다소 사이클 비용이 큰 시스템 콜 인터페이스에 대한 대안이다. 하지만 여러분만의 vDSO를 어떻게 만들어 내는지 설명하기 전에, 운영체제 길을 잠깐 산책하면서 vDSO의 기본, 즉 그것이 무엇이며 왜 유용한지를 다룬다. 이 글의 주된 목적은 Linux 커널에 사용자 정의 vDSO를 추가하는 방법과, 그 노력의 결실을 사용하는 방법을 보여 주는 것이다. 이 글은 vDSO 101을 의도한 것이 아니다. 더 깊은 정보가 필요하다면 이 글의 Resources 섹션에 있는 링크들을 참고하라.
vDSO Basics
사용자 공간(userland) 애플리케이션과 커널 사이의 전통적인 통신 메커니즘은 시스템 콜(system call)이라고 불리는 것이다. 시스템 콜은 소프트웨어 인터럽트로 구현되며 사용자 공간 애플리케이션에게 어떤 커널 기능을 제공한다. 예를 들어 gettimeofday()와 fork()는 둘 다 시스템 콜이다. 시스템 콜이 존재하는 이유는 Linux 커널이 메모리의 두 개 주요 구역으로 나뉘어 있기 때문이다: userland와 kernel land. userland는 dæmon과 서버를 포함한 일반 프로그램들이 실행되는 곳이다. kernel land는 커널이 프로세스를 스케줄링하고 커널 특유의 멋진 마법을 모두 수행하는 곳이다. 메모리에서의 이러한 분리는 사용자 애플리케이션과 커널 사이에 안전 장벽으로 작동한다. 사용자 애플리케이션이 커널을 만질 수 있는 유일한 방법은 시스템 콜 통신을 통해서다. 따라서 커널의 견고함과 무결성은 사용자 공간 접근을 허용하는 제한된 루틴 집합, 즉 시스템 콜들에 의해 보호된다.
시스템 콜을 수행하려면 커널은 메모리 컨텍스트를 왔다 갔다 해야 한다: userland CPU 레지스터를 저장하고, 시스템 콜 인터럽트 벡터(시스템 콜 벡터는 부팅 시 초기화된다)에서 해당 시스템 콜을 찾아서, 그 시스템 콜을 처리한다. 시스템 콜이 kernel land에서 처리되면, 커널은 이전에 저장해 둔 userland 컨텍스트로부터 레지스터를 복원해야 한다. 이것으로 시스템 콜이 완료된다. 하지만 상상할 수 있듯이 이는 공짜로 치러지는 일련의 사건이 아니다. 이런 특별한 형태의 함수 호출을 하기 위해 수많은 사이클이 소모된다.
이러한 분할은 보안 관점에서는 훌륭해 보이지만, 항상 가장 효율적인 통신 수단을 제공하는 것은 아니다. 어떤 데이터를 쓰지 않고 커널에 저장된 값을 단지 반환하기만 하는 gettimeofday() 같은 특정 함수들은 성격상 비교적 안전하며, 요청하는 사용자 공간 애플리케이션이 커널에 위협을 가하지 않는다. 안전한 함수라면 메모리 장벽 탱고를 출 필요가 없으면 좋지 않을까? vDSO로 가능하다!
아마도 vDSO가 전통적인 시스템 콜 대신 프로그램 안에 처음부터 어떻게 들어가게 되는지 궁금할 것이다. vDSO 훅은 glibc 라이브러리를 통해 제공된다. 링커는 gettimeofday()처럼 vDSO 버전을 동반하는 루틴이 있을 경우 glibc의 vDSO 기능을 링크한다. 프로그램이 실행될 때 커널이 vDSO를 지원하지 않으면 전통적인 시스템 콜이 수행된다. 이 vDSO 기능 테스트는 glibc에서 링크된 코드가 제공한다. 물론 집에서 만든 vDSO를 실행하기 위해 glibc를 난도질하고 싶지는 않을 것이다. 아래에서 설명하는 vDSO 생성 방법은 glibc 수정이 필요 없다. 대신 예상대로 커널을 손대는 것에 의존한다.
Cluck, Cluck...vDSO
이런 안전한 시스템 콜들은 각 실행 중인 프로세스의 메모리에 매핑될 수 있는 가상 메모리의 한 페이지에 구현할 수 있다. 이 구현은 공유 라이브러리처럼 다른 동적 공유 객체들이 프로세스에 매핑되는 방식과 유사하다. 실제로 메모리에서 그 페이지를 추출해 디스어셈블하면 결과는 공유 라이브러리 ELF가 된다. 즉 vDSO는 그냥 공유 라이브러리다(마법을 깨서 미안하다). 이 안전한 시스템 콜 루틴들의 페이지가 사용자 공간 애플리케이션에 상주하면, 프로그램은 그 호출을 수행하면서 전통적인 시스템 콜이 요구하는 user/kernel 세그먼트 사이 메모리 점프 오버헤드를 견딜 필요가 없다. 완벽한 예가 gettimeofday()다. 이 루틴은 타이밍에 민감할 뿐 아니라 높은 빈도로 사용되는 경우가 많다. 커널이 메모리 세그먼트를 오가는 데 시간이 걸린다는 점을 생각해 보라. 클록을 샘플링한 뒤에도 메모리 세그먼트를 다시 뒤집는 데 사이클을 써야 한다. 이 시간이 길어질수록 반환되는 시간 값의 정확도는 떨어진다.
Let's Get Frying'
이론과 두루뭉술한 얘기는 이쯤 하고, 이 글의 핵심—여러분만의 vDSO 만들기—로 들어가 보자. 이 글은 2.6.37 Linux 커널을 사용하는 64-bit x86 프로세서를 가정한다. 아마도 놀랄 만큼 쉽다는 점에 놀랄 것이다. 전통적인 시스템 콜을 만드는 것보다도 덜 복잡하다. 혼란스러운 부분은 커널과 사용자 공간 사이에서 변수로 데이터를 공유하려 할 때다.
뭔가 기본적인 일을 하는 시스템 콜을 만들어 보자. 예컨대 짐승의 수, 666이라는 정수 값을 만들어 내는 것이다. 설명 목적상 이 함수를 number_of_the_beast()라고 부르자. 진짜 짐승의 수가 고정된 값인지 확신할 수 없으니(짐승도 바뀔 수 있다), 이 함수가 그렇게 하도록 하자. 즉 짐승의 수가 무엇인지 알려 주는 것이다. (대통령처럼 몇 년마다 바뀔 수도 있다.) linux-2.6.37/arch/x86/vdso/에 vnumber_of_the_beast.c 파일을 만들고, 그 안에 함수를 정의하라:
#include <asm/linkage.h>
notrace int __vdso_number_of_the_beast(void)
{
return 0xDEAD - 56339;
}
여기서 흥미롭거나 흔치 않은 것은 notrace 매크로뿐이다. 이는 linux-2.6.37/arch/x86/include/asm/linkage.h에서 다음과 같이 정의된다:
#define notrace __attribute__((no_instrument_function))
위의 GNU 확장은 gcc 컴파일러에게 이 함수를 컴파일할 때 프로파일링 피드백을 지원하는 훅을 제외하라고 지시한다. notrace 매크로를 제거하고 컴파일 시 gcc 플래그 -finstrument-functions를 전달하면 프로파일링 피드백을 내장할 수 있다(Resources에 나열된 GCC Manual을 보라).
또한 컴파일러에게 number_of_the_beast라는 사용자 공간에서 접근 가능한 함수도 링크하라고 알려야 하는데, 이는 weak 심볼이기도 하다. weak 심볼은 런타임까지 해석되지 않는 함수 호출 같은 데이터를 나타낸다. “weak”이라는 말은 그 심볼이 오버라이드될 수 있음을 의미한다. 심볼이 존재하지 않으면 경고가 나오지 않는데, 이 경우에는 심볼이 없어도 괜찮기 때문이다. alias는 로컬 __vdso_number_of_the_beast를 외부에서 접근 가능한 버전인 number_of_the_beast에 연결한다. 앞서 추가한 함수 바로 뒤에 다음 조각을 추가하라:
int number_of_the_beast(void)
__attribute__((weak, alias("__vdso_number_of_the_beast")));
이제 링커 스크립트에 몇 가지 조각을 던져 넣기만 하면, 커널이 빌드될 때 여러분의 코드가 빌드되고 vdso.so 공유 객체에 링크된다. 여러분이 vDSO를 사용하는 코드를 작성할 때 이 파일을 훅으로 사용할 것이다. 이제 텍스트 에디터를 열고 linux-2.6.37/arch/x86/vdso/vdso.lds.S를 수정해 방금 추가한 함수 이름들을 넣어라:
VERSION {
LINUX_2.6 {
global:
clock_gettime;
__vdso_clock_gettime;
gettimeofday;
__vdso_gettimeofday;
getcpu;
__vdso_getcpu;
/* ADD YOUR VDSO STUFF HERE */
number_of_the_beast;
__vdso_number_of_the_beast;
local: *;
};
}
마지막으로 한 가지 더 있다. 컴파일러에게 vnumber_of_the_beast.c를 실제로 컴파일하라고 알려야 한다. 이를 위해 linux-2.6.37/arch/x86/vdso/Makefile에 정보를 조금 추가하라. .c 확장자 대신 .o 확장자를 사용해 파일 이름을 넣으면 된다. 그러면 make의 마법과 블랙 매직을 통해 컴파일 시점에 컴파일된다. 다시 텍스트 에디터를 열고 vobjs-y 변수의 오브젝트 파일 목록에 이름을 추가하라. 결과는 대략 다음과 같아야 한다:
# files to link into the vdso
vobjs-y := vdso-note.o vclock_gettime.o vgetcpu.o
↪vvar.o vnumber_of_the_beast.o
And Now Some Special Sauce
vDSO가 사용자 공간에서 동작한다면 커널 영역 변수를 어떻게 접근할까? 결국 vDSO가 커널 정보를 제공하려면 userland/kernel land 메모리 세그먼트를 넘어야 하지 않을까? 그렇다면 메모리 컨텍스트를 뒤집는 과정이 vDSO를 무용지물로 만들지 않을까? 음, 그건 사용자 공간 버전(즉 vDSO 버전)이 커널 데이터를 어떻게 접근하느냐에 달렸다. gettimeofday()의 경우, 커널이 업데이트하고 사용자 공간(vDSO 버전)이 읽을 수 있도록 특별한 시간 변수를 메모리에 매핑한다. 커널은 자신이 아는 시간 정보를 그 변수로 복사하기만 하고, vDSO 호출은 메모리 세그먼트를 넘나드는 오버헤드를 절약하며 그 정보를 읽는다. 커널 변수를 추가하거나 접근하는 일은 기본 vDSO 함수에 비해 꽤 복잡하지만, vDSO의 목적이 변수로 제공되는 커널 정보에 접근하는 것이므로, 그 방법을 간단히 개괄해 두는 편이 좋겠다.
설명 목적으로 kernel land에 존재하지만 userland에서 읽히는 값을 하나 추가해 보자. 물론 앞에서는 이 신비한 숫자가 바뀔 수 있으니 함수를 구현하라고 했다. 함수는 있지만, 지금 아는 건 값뿐이고 미래에 무엇으로 바뀔지 모른다. 이제 그 함수가 상수가 아닌 값을 반환하게 만들자. 와, 이 사용 사례는 점점 더 특이해진다. 더 말하자면, 커널 요청에 따라 이 변수를 업데이트하게 하자. 커널은 linux-2.6.37/arch/x86/kernel에 있는 update_vsyscall() 함수에서 vDSO 변수들을 업데이트한다.
만약 const int vnotb = 666;처럼 선언한다면 그 안에 잡힌 값은 설정되지 않을 것이다(자세한 설명은 뒤에서 한다).
짐승의 신비한 수 자체를 값으로 정의하자. 이를 vnotb라고 부르겠다. 이 숫자는 kernel land에 존재할 것이며, 시간 같은 다른 유용한 값들처럼 효율적인 gettimeofday() vDSO가 획득하는 대상이 된다. 바로 여기에서 vDSO의 진짜 마법이 드러난다.
linux-2.6.37/arch/x86/vdso 안에서 계속 진행하며 여기의 좋은 것들을 모두 수정하자. 먼저 VEXTERN() 매크로로 변수를 선언한다. vextern.h에서 다른 선언들 옆에 다음 선언을 추가하라:
VEXTERN(vnotb)
이 매크로는 여러분이 관심 있는 값에 대한 포인터인 변수를 만들고 vdso_ 접두사를 붙인다. 본질적으로 vnotb를 int *vdso_vnotb;로 선언한 것이다.
vextern.h는 다음을 언급한다:
vDSO에서 사용되는 모든 커널 변수는 메인 커널의 vmlinux.lds.S/vsyscall.h/proper__section에 export되어야 하며 vextern.h에 들어가야 하고 vdso 접두사를 가진 포인터로 참조되어야 한다. 메인 커널은 나중에 그 값들을 채운다(comment in linux-2.6.37/arch/x86/vdso/vextern.h).
이제 vDSO 코드의 일부(사용자 공간 쪽)와 커널-사용자 공간 매핑이 준비되었으니, 이를 사용해 보자. vget_number_of_the_beast() 함수에서 그 값을 반환하자:
notrace int __vdso_number_of_the_beast(void)
{
return *vdso_vnotb;
}
그 값을 선언하는 헤더 vextern.h를 추가하는 것을 잊지 말라. 또한 후자가 참조하는 일부 데이터를 해석해 줄 추가 헤더 vgtod.h도 필요하다:
#include <asm/vgtod.h>
#include "vextern.h"
마무리로, 커널이 이 변수에 대해 알도록 해서 데이터를 밀어 넣을 수 있게 해야 한다. 커널이 사용자 공간에 값을 제공해야 한다. 위에서 지정한 주소에 매핑해 두었지만, 대령님(콜로넬 샌더스)이 그 안에 데이터를 밀어 넣지 않으면 별 의미가 없다. 한 디렉터리 위로 올라가야 한다(맞다, 그리 단순한 과정은 아니다). linux-2.6.37/arch/x86/kernel로 이동하라. 링크가 이 값을 알 수 있어야 커널과 사용자 공간 사이를 매핑할 수 있으므로, 그 부분을 손봐야 한다. vmlinux.lds.S를 수정하고 vgetcpu_mode 조각 뒤에 다음을 추가하라(참고로 vgetcpu_mode 앞뒤 어디에 넣어도 필수는 아니지만 찾기 쉬운 위치다):
.vnotb : AT(VLOAD(.vnotb)) {
*(.vnotb)
}
vnotb = VVIRT(.vnotb);
이는 vnotb 심볼을 vnotb 변수와 링크한다. 커널 영역이 접근하고 쓸 수 있도록 주소 공간에 변수를 설정한다. 위의 AT, VLOAD, VVIRT 매크로는 vnotb에서 올바른 데이터 조각이 참조되도록 주소를 수정하는 일을 한다.
이제 kernel land가 쓸 값을 선언해야 한다. linux-2.6.37/arch/x86/include/asm/vsyscall.h에서 이 녀석과, 방금 추가한 링커 스크립트 엔트리를 통해 삽입될 섹션을 선언하라:
#define __section_vnotb __attribute__ ((unused,
↪__section__ (".vnotb"), aligned(16)))
이 파일에서 언급했듯이, 커널 영역 변수를 선언해서 커널이 그곳에 쓸 수 있게 해야 한다. 약간 더 읽기 좋게 하려면 vgetcpu_mode 선언 옆에 변수를 붙여라:
extern int vnotb;
또한 커널이 읽을 수 있는 값도 정의할 수 있다(내 예제에서는 사용하지 않지만, 커널이 값을 읽어야 한다면 이 변수를 읽으면 된다):
extern int __vnotb;
이제 코드를 넣고 값을 부여하자. 커널은 쓰기 가능한 vnotb를 통해 값을 쓰고, 여러분도 __vnotb를 통해 커널과 사용자 공간 사이의 공유 메모리에서 그 값을 읽을 수 있다. 여러분은 커널 영역 버전의 변수(쓰기 가능)에 값을 쓸 것이다. linux-2.6.37/arch/x86/kernel/vsyscall_64.c에서, 가능하면 모든 #include 헤더 뒤쪽이면서 다음 조각 바로 뒤에:
int __vgetcpu_mode
__section_vgetcpu_mode;
다음을 추가하라:
int __vnotb __section_vnotb;
기억하라. 링커로 값을 설정하는 트릭을 썼다. extern처럼 전역으로 값을 설정하면 값이 들어가지 않는다. 링커가 그것을 덮어쓸 것이다. 이 값은 컴파일 타임에 정적으로가 아니라 런타임에 설정해야 한다. 커널이 업데이트할 때 이 값을 설정하려면 linux-2.6.37/arch/x86/kernel/vsyscall_64.c의 update_vsyscall() 루틴을 다음으로 수정하라:
vnotb = 666;
이 문장은 앞서 vsyscall.h에 선언한 값을 정의한다.
Compiling, Linking and Running
잠깐, vDSO 추가는 이게 전부인가? 음, 그렇다. 물론 그 함수가 C 라이브러리(우리 경우 glibc)가 지원하는 것이라면, vDSO 탐지와 실제 호출을 수행하도록 그것을 해킹할 수 있다. 하지만 glibc는 건드리지 않겠다고 했다. 그리고 실제로 그럴 필요도 없다. 코드를 동작시키는 것은 꽤 간단하다. 위에서 설명한 조각들이 모두 제자리에 들어갔으니, 이제 빌드를 시작할 시간이다. 평소처럼 커널을 구성하고 컴파일하라:
make menuconfig
make bzImage
make modules
make modules_install
이제 새로 수정한 vDSO 커널을 설치하고 부팅하라. 부팅이 끝나면 몇 가지를 테스트할 시간인데, 주로 방금 추가한 vDSO 관련 부분이다. vDSO 호출을 실행할 테스트 케이스를 컴파일해 보자:
/* notb.c */
#include <stdio.h>
int main(void)
{
int notb = number_of_the_beast();
printf("His number is %d\n", notb);
return 0;
}
그다음 위 코드를 다음과 같이 컴파일하라:
gcc notb.c -o notb vdso.so
링크하는 파일은 vdso.so이며, 커널 호출을 수행하는 데 필요한 심볼 해석을 제공한다. number_of_the_beast()의 커널 버전이 호출되는데, vdso.so 안에 있는 그 함수의 코드가 완전히 다르더라도 그렇다. vdso.so는 어디에 있을까? 커널을 빌드한 후 커널 빌드 디렉터리 안에 있다: linux-2.6.37/arch/x86/vdso/vdso.so.
런타임에 프로그램이 number_of_the_beast를 실행하면, vdso.so 파일 안의 number_of_the_beast() 버전이 아니라 커널 코드가 호출된다. 커널을 수정해서 예컨대 number_of_the_beast()가 42를 반환하게 하더라도, 그 커널을 로드하지 않는 한 여전히 666을 얻는다. 위의 테스트 예제를 더 최신(42로 수정된) vdso.so로 컴파일하더라도 마찬가지다.
vdso.so 파일을 얻는 또 다른 방법은 실행 중인 실행 파일에서 vDSO 메모리를 추출하는 프로그램을 작성하는 것이다. 온라인에 이를 설명하는 자료가 많지만, 여기서는 간략히 설명한다. 모든 실행 중인 프로세스의 메모리에 매핑되는 vDSO 페이지는 Linux의 주소 공간 배치 난수화(ASLR) 덕분에 실행 중인 프로세스에서 비결정적인 메모리 범위에 있을 수 있다. 이 주소를 얻기 위해 실행 중인 프로그램은 /proc/self/maps 파일에서 자신의 메모리 정보를 확인할 수 있다. 그 안에는 [vdso]라는 텍스트가 있는 라인이 있다. 그 라인은 실행 중인 프로세스에서 vDSO 페이지의 주소 범위를 담고 있다. 예를 들어 다음을 실행할 수 있다:
cat
/proc/self/maps
이 명령을 여러 번 실행하면(커널이 이를 지원한다면) [vdso]의 주소 범위가 매번 달라지는 것을 볼 수 있는데, 이는 주소 공간 배치 난수화 때문이다.
출력은 대략 다음과 같아야 한다:
...
7fff40d71000-7fff40d72000 r-xp 00000000 00:00 0 [vdso]
...
위 범위는 방금 실행한 cat 프로세스에서 vDSO 페이지의 주소 범위가 7fff40d71000에서 시작해 7fff40d7200에서 끝난다는 것을 보여 준다. 시작과 끝 범위를 빼면 0x1000 또는 4096바이트가 된다. 4096은 커널에서 자주 사용되는 페이지 크기다. Listing 1은 실행 중인 커널에서 vDSO를 추출하는 코드이며, Resources에 나열된 "Examining the Linux VDSO" 글의 코드를 바탕으로 한다.
동적 객체 심볼을 간단히 덤프하는 것은 다음으로 할 수 있다:
objdump -T vdso.so
공유 라이브러리도 ELF이므로 readelf 도구 역시 vdso.so에 사용할 수 있다.
Listing 1. 실행 중인 커널에서 vDSO 추출하기
/* extract_vdso.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
char buf[256], *mem;
const char *range_name;
FILE *rd, *wr;
long long start_addr, end_addr;
/* Open file for writing the vdso data to */
if (argc != 3)
{
fprintf(stderr,
"Usage: %s <file> <string>\n"
"\t<file>: File to write the vdso data to.\n"
"\t<string>: Name of the mapped in region, e.g. vdso\n",
argv[0]);
abort();
}
range_name = argv[2];
if (!(wr = fopen(argv[1], "w")))
{
perror("Error: fopen() - output file");
abort();
}
/* Get this process' memory layout */
if (!(rd = fopen("/proc/self/maps", "r")))
{
perror("Error: fopen() - /proc/self/maps");
abort();
}
/* Find the line in /proc/self/maps that contains
the substring [vdso] * */
while (fgets(buf, sizeof(buf), rd))
{
if (strstr(buf, range_name))
break;
}
fclose(rd);
/* Locate the end memory range for [vdso] */
end_addr = strtoll((strchr(buf, '-') + 1), NULL, 16);
/* Terminate the string so we can get the start
address really easily * */
*(strchr(buf, '-')) = '\0';
start_addr = strtoll(buf, NULL, 16);
/* Open up the memory page and extract the vdso */
if (!(rd = fopen("/proc/self/mem", "r")))
{
perror("Error: fopen() - /proc/self/mem");
abort();
}
/* Hop to the vdso portion */
fseek(rd, start_addr, SEEK_SET);
/* Copy the memory locally and then move it to the file */
mem = malloc(end_addr - start_addr);
if (!fread(mem, 1, end_addr - start_addr, rd))
{
perror("Error: read() - /proc/self/mem");
abort();
}
/* Write the data to the specified output file */
if (!fwrite(mem, 1, end_addr - start_addr, wr))
{
perror("Error: fwrite() - output file");
abort();
}
free(mem);
fclose(rd);
fclose(wr);
printf("Start: %p\nEnd: %p\nBytes: %d\n",
(void *)start_addr, (void *)end_addr, (int)(end_addr -
↪start_addr));
return 0;
}
Security Implication
커널을 건드릴 때마다 보안적 함의를 고려해야 한다. 여러분만의 vDSO 호출을 만들어서 누군가를 "털" 수 있다고 생각한다면 다시 생각하는 편이 좋다. vDSO를 추가하려면 사용자가 자기 커널을 직접 구워야 하므로, 그들이 손상시킬 수 있는 대상은 그들의 시스템과 그 시스템의 사용자들뿐이다. 물론 커널 리소스를 만지는 일은 항상 많은 숙고를 거쳐야 한다. vDSO 관련 장난감은 userland에서 동작하지만, 여러분의 vDSO는 커널 데이터에 접근할 수 있다. 그리고 커널은 vDSO 데이터를 읽을 수 있다. 이는 우려가 될 수 있지만, 여기서는 악용 가능한 것이 있는지 찾아보는 연습 문제로 여러분에게 맡기겠다.
마지막으로 이 글은 여러분만의 vDSO를 만들어 요리하는 방법을 짧게 ‘원-투’로 보여 준 것뿐이다. 이제 스모킹 커널을 하나 만들어 보라.
Resources
GNU/Linux Kernel. 2.6.37: http://www.kernel.org
"6.30 Declaring Attributes of Functions" (GCC Manual): http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
"Weak Symbol" (Wikipedia): http://en.wikipedia.org/wiki/Weak_symbol
"Examining the Linux VDSO" (Truth, Computing and Fail): http://anomit.com/2010/04/18/examining-the-linux-vdso
Johan Peterson's "What is linux-gate.so.1?": http://www.trilithium.com/johan/2005/08/linux-gate
Matt Davis' "Linux syscall, vsyscall, and vDSO...Oh My!": http://davisdoesdownunder.blogspot.com/2011/02/linux-syscall-vsyscall-and-vdso-oh-my.html