Microsoft Vista i zmienne środowiskowe.
lipiec 31st, 2007Hi,
Podczas pisania (to jeszcze nie jest czas przeszły ukończony ;>) tutoriala poświęconego językowi skryptowemu .bat (batch) natknąłem się na pewne ciekawe niedopatrzenie, lub jak kto woli - feature, w Windows Vista, dotyczący zmiennych środowiskowych. Jak każdy kto pisał kiedyś coś w języku .bat wie, istnieje możliwość deklarowania nowych zmiennych środowiskowych, dla własnego procesu oraz procesów potomnych, za pomocą instrukcji SET, np. SET NAZWA=WARTOSC. W skryptach .bat maksymalna wielkość wartości (w znakach) zależy od interpretera, a dokładniej od ograniczenia maksymalnej wielkości wiersza poleceń. Interpreter Windows NT 4 oraz Windows 2000 posiadał ograniczenie 2048 znaków, Windows XP oraz 2003 zwiększyło limit do 8192, natomiast Windows Vista nie nakłada ograniczenia na ilość znaków w przypadku komendy SET.
Prosty skrypt w którym najpierw tworzymy dłuuugą zmienną, a potem ją wypisujemy za pomocą komendy SET NAZWA pokazuje że maksymalna wielkość zmiennej (wraz z jej nazwą oraz znakiem równości) wynosi 8192 bajty, przy czym zjadany jest nawet znak końca linii przy wypisywaniu.
Ah, ale czy na pewno o tym mówi taki skrypt ? Nie do końca. Mianowicie skrypt mówi że maksymalna długość linii przewidziana w poleceniu SET wynosi 8192. Jak więc ustalić maksymalną długość zmiennej ? Można zajrzeć do listingu interpretera CMD.EXE i poszukać implementacji funkcji SET. W implementacji znajdziemy funkcję SetEnvironmentVariablW, która najwyraźniej jest odpowiedzialna za ustawienie zmiennej. Rzut okiem na MSDN i wiemy że:
The total size of the environment block for a process may not exceed 32,767 characters.
Czyli dowiedzieliśmy się że maksymalna wielkość bloku zmiennych środowiskowych dla procesu wynosi 32767 znaków. OK. Ale czemu by tego nie przetestować ? Można stworzyć więc prosty programik który, korzystając z trzeciego parametru funkcji main(), wypisze wszystkie zmienne środowiskowe, czyli zadziała tak jak SET. Programik może wyglądać następująco:
#include <stdio.h>
int
main(int argc, char **argv, char **envp)
{
while(*envp)
{
puts(*envp);
envp++;
}
return 0;
}
Tworzymy ponownie zmienną środowiskową, powiedzmy taką o długości wstępnej 40000 znaków, i zgodnie z dokumentacją oczekujemy że jej wielkość będzie większa niż 8192 znaki które wypisuje SET, ale mniejsza niż 32767 znaków. Odpalamy i… okazuje się że zmienna ma 40000 znaków. Huh.
Jak wspomniałem wcześniej, SET korzysta z SetEnvironmentVariable. Można więc napisać prosty program który upewni się że ta funkcja faktycznie może stworzyć zmienną powyżej 32767 znaków:
#include <stdio.h>
#include <windows.h>
int main(void)
{
static char big[40000 + 1];
int i;
for(i = 0; i < sizeof(big) - 1; i++)
big[i] = 'A';
if(!SetEnvironmentVariable("big", big))
printf("ERROR %i\n", GetLastError());
return 0;
}
Kompilujemy, odpalamy, i… Nie działa, ERROR 87 czyli ERROR_INVALID_PARAMETER. Ciekawe. Po porównaniu listingów okazuje się oczywiście że my używamy funkcji A, natomiast interpreter korzysta z funkcji W, czyli wersji wide-char. Teoretycznie obie funkcje powinny dawać identyczne rezultaty, jednak czy na pewno tak jest ? Przetestujmy:
#include <stdio.h>
#include <windows.h>
static char big_c[1024*1024];
static short big_s[1024*1024];
int
main(void)
{
int i;
for(i = 0; i < 1024*1024 - 1; i++)
{
big_c[i] = 'A';
big_s[i] = 'A';
}
printf("W: %i\n", SetEnvironmentVariableW(L"S",big_s));
printf("A: %i\n", SetEnvironmentVariableA("A",big_c));
return 0;
}
Windows Vista pokazała wynik W: 1 oraz A: 0. Skąd ta różnica? Odpowiedzią która się nasuwa jest “panowie z MS zapomnieli sprawdzania wielkości w wersji W, a mają ją w wersji A”. Natomiast tak nie do końca jest. Jak wynika z analizy listingu obu funkcji, nie ma w żadnej sprawdzania wielkości. Skąd więc błąd w funkcji A? Otóż wersja ANSI funkcji, jak to jest w większości przypadków, konwertuje stringi ANSI na stringi Wide-Char, po czym wywołuje funkcję W, lub funkcję realizującą dane działanie która znajdzuje się w NTDLL.DLL. W tym wypadku mamy do czynienia z tym drugim, mianowicie obie funkcje wywołują RtlSetEnvironmentVar (A zachacza jeszcze o RtlSetEnvironmentVariable, ale to bez znaczenia). Stąd wniosek, że błąd jest generowany w momencie konwersji ANSI na Wide-Char. I faktycznie, w funkcji RtlInitAnsiStringEx znajdujemy następujące instrukcje:
cmp eax, 0FFFEh ; w EAX jest długość stringu
ja loc_ERROR
[...]
ERROR:
mov eax, 0C0000106h ; STATUS_NAME_TOO_LONG
pop ebp
retn 8
Najwyraźniej funkcja RtlInitAnsiStringEx nie dopuszcza przydługawych stringów. Cóż.
Jakie są tego implikacje? Raczej nic poważnego, jako że zmienne środowiskowe utworzone w danym procesie i tak są przekazywane tylko do jego dzieci. Ale jednak są, więc przykładowo jeśli utworzymy zmienną o długości 100MB i uruchomimy 20 kalkulatorów to zużyjemy całą pamięć systemu (taki eksperyment u mnie skończył się 10 minutowym zawieszeniem systemu, po tym czasie system się jednak odwiesił i dalej działał już normalnie; pagefile widać długo był tworzony). Inną implikacją mogą być problemy jeśli program-dziecko korzysta nieumiejętnie ze zmiennych środowiskowych (strcpy(zmienna, envp[2]) ;>). Tak jest na przykład w przypadku interpretera poleceń Windows NT4, który, jeśli uruchomiony w momencie gdy w środowisku jest zmienna powyżej określonej wielkości, po wydaniu komendy SET najzwyczajniej w świecie się wysypał (ACCESS_VOLATION). Na szczęście nikt nie używa interpretera z NT4 na Viście.
Z moich testów wynika iż problem dotyczy jedynie Windows Vista. Na XP ani funkcja A, ani funkcja W nie pozwalają na alokacje przydługawych zmiennych środowiskowych.
K tyle. G.C.
Recently
Archives
Categories
