userfaultfd() is here

I noticed a new (as of Linux kernel 4.3+) userfaultfd() system call while poking around make menuconfig today and, despite thinking that it may be complexity creep at first, found lots of good information about really interesting use-cases it enables.

The manpage is here, including a sample program, and gives a good idea about how userfaultfd() works.

To get more context and an overview of application ideas I recommend start with this presentation by Andrea Arcangeli.

To sum it up:

  • userfaultfd() is key to enabling live VM migrations and research-level stuff like memory externalization
  • memory constrained environments, like Android devices, need a way to reclaim memory from processes without crashing them.
    userfaultfd() makes it possible for the process to notice that its volatile pages are gone. It seems similar to how weak references work in many programming languages, but now with memory pages?
  • apparently more efficient snapshoting (think Redis), but I haven’t thought it through yet to understand how!

The presentation also mentions JIT improvements, but it’s a broader (and exciting) subject. This essay provides a deep look from the perspective of a LISP compiler/VM and touches upon the GC implications.

These awesome posts show how the HotSpot JVM currently uses memory protection/SIGSEGV:

More JVM Signal tricks – Thread control via mprotect
SIGSEGV as control flow – How the JVM optimizes your null checks

I wonder if userfaultfd() is coming to HotSpot?

Database queries that took small multiples of 100ms

I had a curious troubleshooting task last year. A developer came up asking if I could explain why logs of some Ruby Sidekiq workers often showed simple MySQL queries taking almost exact multiples of 100ms to complete. The numbers would be like: 105ms, 201ms, 402ms, 205ms, and so on…

Half an hour of interrogation, strace, top and browsing through MRI source code led to the following answers:

  • each Sidekiq process has many threads
  • the purpose of having so many threads was to fully utilize the CPU and memory running tasks that spent most of their time waiting for remote systems to respond
  • most of the jobs were quick and simple, but some could take many seconds of CPU time to process

We also observed an obvious correlation between the Sidekiq worker CPU spiking to 100% for extended periods of time and the round SQL query times being logged.

But look, here’s our 100ms!

Minutes later we stumbled upon this clear explanation. I’m quoting the relevant part:

Every 100ms the timer thread sets an interrupt flag on the thread currently holding the GIL.

Deep in a file called vm_eval.c is the code for handling Ruby method calls. It’s responsible for setting up the context for a method call and calling the right function. At the end of a function called vm_call0_body, right before it returns the return value of the current method, these interrupts are checked.

If this thread’s interrupt flag has been set, then it stops execution on the spot, before returning its value. Before executing any more Ruby code, the current thread will release the GIL and callsched_yield. sched_yield is a system function prompting the thread scheduler to schedule another thread.

Aha! So it works as follows:

  1. Thread A takes a note of current time T1, sends a SQL query and waits for results. This releases the Global Interpreter Lock and lets the VM schedule a different thread.
  2. The VM schedules Thread B to run. Unfortunately Thread B is going to take seconds of CPU time.
  3. Milliseconds later the TCP stack has the answer from MySQL ready, but the requesting thread A is still waiting for its turn on the CPU.
  4. After 100ms Thread B is interrupted. Another CPU-heavy thread C gets scheduled.
  5. After another 100ms, Thread C is interrupted and Thread A gets its slice of the CPU time.
  6. Thread A takes the current time T2 and logs it took T2 – T1 = 205ms to get the results of its simple SQL query.

Having found the answer, we left the system mostly as it was. It was optimized for throughput, not response times, so long job pauses had no real impact.

The obvious lessons are:

  • Measure and log the right thing. In this case it wasn’t the SQL query that took hundreds of milliseconds.
  • Know your runtime environment. 🙂
  • Distribution of the times it takes to handle requests is important.
  • Global Interpreter Locks are not pretty.

Getting Atom Zombie Smasher to run under Ubuntu 12.04

There’s a number of forum posts asking for help with Atom Zombie Smasher crashing after the loading screen with recent versions of Linux distros on the Internet, but I’ve been unable to find one with a solution – so here it is.

In short, there seems to be a race condition somewhere in the initialization code that makes the process terminate in xcb code. Not having the sources available, my solution was to set the game’s CPU affinity to just one core, thus greatly reducing the chances of the crash. It’s quite reliable (9/10 I’d say), but don’t be surprised if it does crash on some attempts!

To apply the “fix”, change the last line in the data/atomzombiesmasher script to:

MONO_LOG_LEVEL="debug" taskset 0x00000001 ./mono ./release.exe "$@"

Good luck!

S.M.A.R.T. data visualization

Today, I got worried about two consecutive changes of the ‘ECC Recovered Errors’ parameter reported by SMART on my desktop machine:

Device: /dev/disk/by-id/scsi-SATA_ST3200827AS_4ND2DF00, SMART Usage Attribute: 195 Hardware_ECC_Recovered changed from 67 to 66


Device: /dev/disk/by-id/scsi-SATA_ST3200827AS_4ND2DF00, SMART Usage Attribute: 195 Hardware_ECC_Recovered changed from 66 to 65

That prompted me to see if there have been any similar events recently. Two lines of shell script were more than enough to reassure me that nothing bad was going on:

grep ECC /var/log/messages | egrep -o 'from [[:digit:]]+' | cut -f2 -d' ' > data

And then with gnuplot:

gnuplot> plot 'data' with lines

I got this:


SMART values graph
SMART values graph

Uruchamianie programu w Linuksie

Note: this is the original paper as I wrote it a long time ago, when I still was a student. I’m publishing it here in Polish, hoping that someone will find it useful. English version is going to be released soon.

Po polsku

Poniższy wpis wyjaśnia jak uruchamiane są programy łączone dynamicznie w Linuksie, choć podaję też przykłady pochodzące z Solarisa. Grupą docelową nie są doświadczeni administratorzy, a raczej osoby dopiero zaczynające swoją przygodę z systemami wywodzącymi się od Uniksa, więc zostawiłem w nim potencjalnie trywialne fragmenty.

Wstęp

Pewnego razu znajomemu administratorowi przydarzyła się taka oto historia: próbując stworzyć sobie “system w systemie” (odseparowany przy pomocy chroota) zabrał się za tworzenie nowej struktury katalogowej dla zagnieżdżonego środowiska. Stworzył więc katalog /newroot, a następnie zaczął wypełniać go kopiami plików z systemu pierwotnego. O tyle niefortunnie, że zamiast skopiować (cp), przeniósł (mv) katalogi /bin oraz /lib (mv), skutecznie psując system. W efekcie tej pochopnej operacji, ważne programy zaczęły zachowywać się, na przykład, w następujący sposób:

host:/newroot/bin/# ./cp
-su: ./cp: Nie ma takiego pliku ani katalogu

Komunikat bardzo mylący, gdyż plik cp oczywiście istniał w katalogu /newroot/bin.

Jak się okazało, istnieje elegancki sposób wybrnięcia z tej sytuacji, lecz by wyjaśnić to zachowanie, musimy zrozumieć systemowe mechanizmy odpowiedzialne za obsługę bibliotek współdzielonych oraz ich rolę w działaniu całego środowiska. Na zakończenie, oprócz pokazania sposobu rozwiązania takiego problemu, pokażę alternatywną metodę częściowego zabezpieczenia się przed awariami związanymi z brakiem lub uszkodzeniem systemowych bibliotek.

Zakładam przy tym, że znasz pojęcia jądra, procesu, bibliotek współdzielonych, przestrzeni adresowej oraz mapowania plików do pamięci, gdyż ich znajomość jest potrzebna do zrozumienia dalszej części tekstu.

Struktura pliku programu

Aby program mógł zostać uruchomiony przez system operacyjny, musi być zapisany w pliku o odpowiedniej strukturze. Aktualnie w świecie systemów uniksowych króluje format ELF (Executable and Linking Format), więc poniższe informacje podane są w jego kontekście. Funkcjonujące dawniej formaty a.out i COFF pomijamy.

Zacznijmy od zbadania przykładowego pliku wykonywalnego (/bin/ls) przy pomocy narzędzia readelf, wypisującego w czytelnej dla człowieka formie informacje zgromadzone w nagłówku oraz sekcjach plików w formacie ELF.

Wynik działania programu powinien wyglądać tak:

% readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4025d0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          87432 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         27
  Section header string table index: 26

Widzimy kolejno:

  1. “Magiczny numer” (Magic), dzięki któremu możliwe jest rozpoznanie formatu pliku
  2. Klasę (Class), określającą architekturę (32 lub 64 bity), do której przystosowany jest plik
  3. Interfejs binarny (OS/ABI), z którego korzysta plik wykonywalny
  4. Rodzaj maszyny (Machine), dla której przeznaczony jest kod
  5. Punkt wejścia (Entry point address), czyli adres, od którego zaczyna się wykonywanie programu
  6. Dane określające liczbę oraz rozmiar dodatkowych nagłówków i sekcji

Oprócz tych ogólnych informacji, w wykonywalnym pliku w formacie ELF znajdują się jeszcze metadane opisujące dokładne wymagania programu, takie jak adresy pamięci, pod którymi mają znaleźć się sekcje pliku oraz wymagane biblioteki wraz z importowanymi z nich symbolami. Przyjrzymy się im w dalszej części wpisu.

Uruchamianie

Uruchamiania łączonego dynamicznie programu jest dość skomplikowanym procesem, który zaczyna się w momencie wywołania funkcji z rodziny exec, zapewniającej różnorodne interfejsy do systemowego wywołania execve, którego rolą jest załadowanie do przestrzeni adresowej procesu kodu nowego programu oraz przygotowanie środowiska do jego wykonania. W przypadku Linuksa, proces ten można przedstawić skrótowo jako listę następujących kroków:

  1. Sprawdzenie formatu pliku wykonywalnego i, jeśli jest on obsługiwany, rozpoczęcie jego przetwarzania
  2. Odczyt części nagłówka pliku, opisującego segmenty programu
  3. Zwolnienie zasobów związanych z poprzednim kontekstem wykonania (m.in. usunięcie przyporządkowania stron pamięci, aktualizacja procedur obsługi sygnałów)
  4. Alokacja pamięci na stos trybu użytkownika, zmienne środowiskowe, argumenty podane z linii poleceń oraz kod i część danych uruchamianego programu
  5. Mapowanie segmentów pliku pod odpowiednie adresy w przestrzeni procesu
  6. Powtórzenie kroku 6 dla interpretera programu (wybranego na podstawie nagłówka)
  7. Zakończenie wykonania wywołania systemowego – proces zaczyna wykonywać kod interpretera, który po zakończeniu pracy wykonuje skok do punktu wejścia programu, rozpoczynając wykonanie właściwego kodu

Powyższy wykaz operacji jest bardzo uproszczony. Pełen opis z dokładnym uwzględnieniem operacji wykonywanych w trybie jądra znaleźć można m.in. w książce “Linux kernel” [1], oraz oczywiście w kodzie jądra.

Skorzystajmy ponownie z narzędzia readelf, by obejrzeć zawartość nagłówka programu (skrócona wersja poniżej):

rogram Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000230 0x0000000000000230  R E    8
  INTERP         0x0000000000000270 0x0000000000400270 0x0000000000400270
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000013f4c 0x0000000000013f4c  R E    200000
  LOAD           0x0000000000014df0 0x0000000000614df0 0x0000000000614df0
                 0x00000000000006c0 0x0000000000000c60  RW     200000
  DYNAMIC        0x0000000000014e18 0x0000000000614e18 0x0000000000614e18
                 0x00000000000001c0 0x00000000000001c0  RW     8
  NOTE           0x000000000000028c 0x000000000040028c 0x000000000040028c
                 0x0000000000000020 0x0000000000000020  R      4
  GNU_EH_FRAME   0x0000000000011f3c 0x0000000000411f3c 0x0000000000411f3c
                 0x0000000000000694 0x0000000000000694  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8
  GNU_RELRO      0x0000000000014df0 0x0000000000614df0 0x0000000000614df0
                 0x0000000000000210 0x0000000000000210  R      1
  PAX_FLAGS      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

Oprócz wpisów określających części pliku oraz odpowiadające im adresy w przestrzeni adresowej procesu, interesuje nas linia INTERP, która określa interpreter programu, który ma zostać załadowany i uruchomiony przez jądro na końcu wywołania execve. Wpis ten ma znaczenie tylko w przypadku plików wykonywalnych (choć może pojawić się też w plikach obiektów). Oczywiście, jeśli interpreter nie zostanie znaleziony, lub jądro nie będzie mogło umieścić go w przestrzeni adresowej procesu, uruchomienie programu się nie powiedzie.

Dygresja: Interpreter znajduje się zazwyczaj w katalogach /lib lub /libexec, dlatego operacje na nich należy wykonywać z odpowiednią ostrożnością – uszkodzenie go może doprowadzić od poważnej awarii systemu.

Wiązanie dynamiczne

Skompilowane (łączone dynamicznie) programy, które można znaleźć w katalogach /bin, /usr/bin, itd., nie nadają się do bezpośredniego uruchomienia (załadowania kodu do pamięci i rozpoczęcie jego wykonywania przez procesor) z dwóch powodów:

  • Ich kod jest niezależny od miejsca w pamięci, w którym się znajduje (PIC – Position Independent Code), a co za tym idzie – początkowo część referencji do pamięci nie wskazuje na jej poprawne w danym kontekście fragmenty. Muszą one zostać odpowiednio ustawione.
  • W przestrzeni adresowej procesu nie jest dostępny wymagany kod, należący do bibliotek współdzielonych, a referencje do symboli (nazwanych adresów, pod którymi znajduje się kod lub obiekty) nie posiadają poprawnych wartości.

Nim sterowanie zostanie przekazane pod adres wskazany przez punkt wejściowy programu, powyższe elementy muszą zostać odpowiednio zainicjowane przez linker dynamiczny. Wykonywanie kodu właściwego programu może rozpocząć się dopiero po przeprowadzeniu odpowiedniego mapowania oraz serii relokacji. Chcąc skupić się na przedstawionym na początku problemie, omówimy tylko kwestię ładowania bibliotek.

Jak już wcześniej wspomniałem, w przestrzeni adresowej procesu musi znaleźć się mapowany kod. Przy pomocy narzędzia strace, śledzącego wywołania systemowe wykonywane przez proces, pokażemy, że linker faktycznie odpowiednio wypełnia przestrzeń adresową przy pomocy funkcji mmap. Wykonajmy strace /bin/ls 2>&1 | less i znajdźmy w wyniku poniższy fragment:

open("/lib/libc.so.6", O_RDONLY)        = 3
read(3, "\177ELF\2\1\1\3>\1\220\334\1"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1293456, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fdab4233000
mmap(NULL, 3399928, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fdab38fd000
mprotect(0x7fdab3a33000, 2093056, PROT_NONE) = 0
mmap(0x7fdab3c32000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x135000) = 0x7fdab3c32000
mmap(0x7fdab3c37000, 16632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fdab3c37000
close(3)                                = 0

Co robi linker?

  • open() – otwiera plik biblioteki w trybie tylko do odczytu; będzie on dostępny pod deskryptorem o numerze 3
  • read() – odczytuje jego nagłówek, by zdobyć informacje o formacie pliku (czy to na pewno biblioteka?) oraz rozmieszczeniu danych
  • fstat() – sprawdza, czy otwarty plik jest zwykłym plikiem, co jest istotne z punktu widzenia późniejszych wywołań mmap(); również sprawdza jego rozmiar
  • mmap() – drugie wywołanie mapuje cały plik w trybie tylko do odczytu z ustawioną flagą pozwalającą na wykonanie – domyślnie kod nie jest zapisywalny; mapowanie jest prywatne, więc ewentualne zmiany nie zostaną wprowadzone do pliku
  • mmap() – trzecie wywołanie mapuje część pliku zezwalając na zapis do tego obszaru pamięci; można się spodziewać, że w tej części znajdują się statyczne dane używane przez bibliotekę
  • close() – zamyka plik

Pozostałe wywołania nie są w tym momencie istotne. Podobne fragmenty pojawią się w wyniku działania strace jeszcze kilka razy, kolejno dla wszystkich ładowanych bibliotek. Wprowadzane zmiany odniesień do pamięci w programie oczywiście nie są widoczne, bo przebiegają bez udziału jądra systemu operacyjnego. Po zakończeniu pracy linkera, zaczyna się wykonywanie kodu programu.

Dygresja: Rozmieszczenie wszystkich segmentów w przestrzeni adresowej procesu można w Linuksie odczytać z pliku /proc/PID/maps, gdzie PID oznacza identyfikator interesującego nas procesu.

Słowo o środowisku

Nim przystąpimy do rozwiązywania postawionego na początku problemu, musimy zdać sobie sprawdę z jednego faktu: linker dynamiczny wykonuje się w przestrzeni adresowej przeznaczonej dla nowo uruchamianego programu. Ma zatem dostęp do jego argumentów oraz zmiennych środowiskowych. Dzięki temu mamy możliwość ingerowania w jego działanie przy pomocy odpowiednich zmiennych środowiskowych. Dwie z nich są szczególnie interesujące:

  • LD_LIBRARY_PATH – określa listę ścieżek, które mają być dodatkowo przeszukiwane pod kątem potrzebnych bibliotek; umożliwia korzystanie z bibliotek znajdujących się w miejscach nie przeszukiwanych standardowo oraz nie określonych przez konfigurację linkera (/etc/ld.so.conf).
  • LD_PRELOAD – określa listę bibliotek, które mają być załadowane przed wszystkimi innymi, określonymi w nagłówkach programu; umożliwia zastępowanie symboli (mówiąc potocznie, umożliwia “podmianę” bibliotecznych funkcji oraz obiektów, gdyż symbole dopasowywane są tylko raz)

Rozwiązanie problemu

Znamy już mechanizm uruchamiania programów w systemach uniksowych, możemy więc przystąpić do analizy przedstawionego na początku wpisu problemu.

Możemy się już domyślić, że dziwnie brzmiący komunikat wyniknął z faktu, że jądro systemu nie odnalazło pliku zawierającego potrzebny mu interpreter, określony w nagłówku pliku programu. Gdyby problem sprowadzał się wyłącznie do braku biblioteki współdzielonej, zostalibyśmy o tym poinformowani w bardziej czytelny sposób, a rozwiązanie byłoby trywialne. Co można zrobić w takiej sytuacji?

  • Przenieść (mv, cp) plik linkera do pierwotnego katalogu
    Jeśli programy mv i cp są łączone dynamicznie, a tak było w tym przypadku, nie można ich użyć do tego celu (nie da się ich uruchomić!)
  • Stworzyć kopię linkera wyłącznie przy użyciu wewnętrznych poleceń powłoki
    Polecenia wewnętrzne, jak nazwa wskazuje, nie uruchamiają zewnętrznych programów. O ile w sytuacji awaryjnej mamy uruchomioną powłokę, można próbować z nich korzystać. Przykładowe zastosowanie poleceń wewnętrznych (read i echo) oraz mechanizmu przekierowań:

    % read tresc < plik
    % echo $tresc > plik2

    Niestety, jądro odmawia załadowania linkera z pliku, który nie ma ustawionych praw wykonania. Z kolei, by je ustawić potrzebny jest program chmod, którego nie możemy uruchomić z powodu braku interpretera. Powyższy przykład ma też dość poważną wadę polegającą na tym, że z dużym prawdopodobieństwem uszkodzi “kopiowany” plik.

Pech. W ogólnym przypadku, z poziomu działającego systemu nie jesteśmy w stanie zaradzić takiej sytuacji – potrzebne jest środowisko ratunkowe (np. płyta CD z możliwym do uruchomienia systemem). Rozwiązanie to, oczywiście, nie jest dobre – w przypadku maszyny pełniącej istotną funkcję nie chcemy powodować dodatkowych przestojów. Co jeszcze nam zostało?

  • Posiadać zestaw podstawowych narzędzi linkowanych statycznie
    Rzeczywiście, jeśli podstawowe narzędzia systemowe skompilowano statycznie, powyższy problem w ogóle się nie pojawi. Jest to doskonałe zabezpieczenie przed problemami związanymi z obsługą ważnych bibliotek współdzielonych.
  • Poinstruować system, gdzie szukać linkera dynamicznego
    Pomysł dobry, ale niewykonalny – taki mechanizm po prostu nie istnieje. Jednak użytkownicy GNU/Linuksa oraz Solarisa mogą skorzystać z dość specyficznej cechy dostępnych w ich systemach linkerów. Pamiętamy, że jest to w gruncie rzeczy normalny program (uruchamiany zazwyczaj przez jądro systemu) z odpowiednim punktem wejściowym. Okazuje się, że w tych systemach istnieje możliwość uruchomienia /lib/ld-linux*.so lub /lib/ld.so.1 z podaniem programu, który ma zostać załadowany i obsłużony przez linker! Zatem mając interesujące nas pliki w katalogu /newroot możemy użyć tej możliwości w następujący sposób:

    % export LD_LIBRARY_PATH=/newroot/lib
    % /newroot/lib/ld-2.3.6.so /newroot/bin/mv /newroot/lib /

    Czyli: ustawiamy niestandardową ścieżkę, pod którą linker ma szukać bibliotek (potrzebna, bo wskazuje miejsce, gdzie znajdują się wymagane przez mv pliki), a następnie uruchamiamy mv, przenosząc potrzebne pliki na ich miejsce! Voila! Przywróciliśmy system do stanu pełnej sprawności!

Do zastanowienia się

Zachęcam do zastanowienia się nad implikacjami faktów przedstawionych w tym wpisie. Pozwolę sobie pomóc, ale przy użyciu metody sokratejskiej:

  • Badając przy pomocy narzędzia ldd najważniejsze programy narzędziowe w systemie – jaka biblioteka pojawia się na liście najczęściej i co z tego wynika?
  • Kod biblioteczny mapowany jest do przestrzeni adresowej procesu z pliku. Czy nadpisanie pliku zawierającego kod biblioteki współdzielonej to dobry pomysł?
  • Zakładając, że powyższe to zły pomysł, to jak zaktualizować bibliotekę współdzieloną jeśli jest ona używana przez działający proces?
  • Rozważmy sytuację, w której program /bin/secret używa funkcji strcmp ze standardowej biblioteki języka C do porównania skrótu wprowadzonego hasła z zakodowanym wzorcem i na podstawie wyniku tej operacji zapewnia dostęp do ściśle tajnych informacji. Stwórz bibliotekę zawierającą funkcję strcmp, która zawsze zwraca wartość wskazującą identyczność argumentów, a następnie spraw, by /bin/secret akceptował każde wprowadzone hasło.
  • Jakie implikacje dla bezpieczeństwa systemu ma powyższy punkt? Czy linker dynamiczny honoruje ustawienia zmiennych LD_PRELOAD i LD_LIBRARY_PATH jeśli wykonywany jest program z ustawionym bitem SUID?

Bibliografia

  • [1] (Daniel P. Bovet, Marco Cesati, O’Reilly, Wydawnictwo RM, Warszawa 2001