![ExpressCard z Firewire używany do debuggowania Windows 10](/img/t_firewire.jpg)
![](/img/con_resize_anim.gif)
Zacząłem trochę na około - od ustalenia jak działa API konsoli pod Windows 10 - wiedziałem, że trochę się tu pozmieniało od kiedy siedziałem nad tym tematem w okolicach Windows XP/Vista/7 (tak na marginesie, strona 38). Z dużych zmian (w skrócie): za okno konsoli nie jest już odpowiedzialny csrss.exe (Client/Server Runtime Subsystem), a, na spółkę, nowy program uruchamiany z uprawnieniami danego użytkownika zajmujący się oknem konsoli - conhost.exe, oraz sterownik odpowiedzialnym za uruchamianie conhost (*cough*) i transport pakietów (RPC) pomiędzy aplikacjami konsolowymi a conhostem - condrv.sys (pseudo-urządzenia \Device\ConDrv\*). To jak dokładnie działają wywołania "konsolowego RPC" jest tematem na oddzielny post - na ten moment wystarczy wiedzieć, że coś takiego ma miejsce.
![Otwarte pseudo-urządzenia \Device\ConDrv\* w cmd.exe oraz urządzenie \Device\ConDrv w conhost.exe.](/img/cmd_conhost.png)
Dodam jeszcze tylko, że przy analizie condrv.sys niezastąpiony okazał się setup, który podrzucił mi j00ru, czyli połączenie dwóch komputerów (w moim wypadku PCta z laptopem) za pomocą firewire - debugowanie (jądra/sterowników) systemu Windows nigdy nie było tak przyjemne; co prawda mój laptop firewire nie posiada, ale ten problem udało się rozwiązać ExpressCardem kupionym w lokalnym Conradzie (patrz też fotka na początku posta).
Wracając do tematu, po przejrzeniu consys.drv, conhost.exe wraz z ConhostV2.dll (na który już niedawno zresztą patrzyłem, przy okazji zabawy z ANSI escape codes pod Windows 10), a także kernel32.dll i KernelBase.dll, problem okazał się leżeć w tym ostatnim.
Jak się okazuje, że conhost nie posiada funkcji GetConsoleScreenBufferInfoEx wypełniającej strukturę CONSOLE_SCREEN_BUFFER_INFOEX - zamiast tego jest funkcja SrvGetConsoleScreenBufferInfo (numer funkcji: 0x2000007), która wypełnia strukturę CONSOLE_SCREENBUFFERINFO_MSG:
struct CONSOLE_SCREENBUFFERINFO_MSG {
/* 8 bajtów nagłówka tutaj */
COORD dwSize;
COORD dwCursorPosition;
COORD srWindow_TopLeft;
WORD wAttributes;
COORD srWindow_WidthHeight;
COORD dwMaximumWindowSize;
WORD wPopupAttributes;
BYTE bFullScreenSupported;
BYTE _padding[3];
DWORD ColorTable[16];
};
Obie wspomniane struktury są podobne, ale nie identyczne. W kontekście omawianego problemu główna różnica dotyczy pola srWindow, które (w CONSOLE_SCREEN_BUFFER_INFOEX) zawiera koordynaty górnego-lewego i dolnego-prawego rogu bufora tekstowego (w "znakach"), a konkretniej, koordynat znaku, który jest widoczny na samej górze konsoli po prawej (zazwyczaj 0,0), oraz koordynat znaku, który jest widoczny na samym dole po prawej (zazwyczaj o jeden mniej niż wynosi szerokość i wysokość konsoli w znakach). Pole to nie występuje CONSOLE_SCREENBUFFERINFO_MSG; zamiast tego są tam dwa inne pola - jedno (oznaczone przeze mnie srWindow_TopLeft) zawiera koordynat górnego lewego rogu, a drugie (srWindow_WidthHeight) mówi o szerokości o raz wysokości konsoli (w znakach). Oczywiście mając jeden zestaw informacji trywialnie jest wyliczyć drugi; w funkcji GetConsoleScreenBufferInfoEx jest to realizowane przez poniższy kod:
if ( lpCSBIEx->cbSize == 96 )
{
// Wywołanie RPC.
v3 = ConsoleCallServer(hConOutput, &v16 /* m.in. CSBIMsg */, 0x2000007, 92);
if ( v3 >= 0 )
{
// Przepisanie informacji (fragment):
// ...
lpCSBIEx->srWindow.Right = CSBIMsg.srWindow_TopLeft.X + CSBIMsg.srWindow_WidthHeight.X - 1;
lpCSBIEx->srWindow.Bottom = CSBIMsg.srWindow_TopLeft.Y + CSBIMsg.srWindow_WidthHeight.Y - 1;
// ...
return result;
}
// ...
Warto zwrócić uwagę na oba wystąpienia - 1, które w powyższym zastosowaniu są jak najbardziej prawidłowe.
Analogicznie do powyższego przypadku, conhost nie posiada funkcji SetConsoleScreenBufferInfoEx; zamiast niej istnieje inna, analogiczna, o nazwie SrvSetScreenBufferInfo (numer funkcji: 0x2000008), które również operuje na wspomnianej wcześniej strukturze CONSOLE_SCREENBUFFERINFO_MSG. Oznacza to, że również funkcja SetConsoleScreenBufferInfoEx musi dokonać konwersji, tyle że w drugą stronę. Jest to realizowane przez poniższy kod:
if ( lpConsoleScreenBufferInfoEx->cbSize == 96 )
{
// Przepisanie informacji (fragment):
// ...
CSBIMsg.srWindow_WidthHeight.X = lpCSBIEx->srWindow.Right - lpCSBIEx->srWindow.Left;
CSBIMsg.srWindow_WidthHeight.Y = lpCSBIEx->srWindow.Bottom - lpCSBIEx->srWindow.Top;
// ...
// Wywołanie RPC.
v8 = ConsoleCallServer(hConOutput, &v10 /* m.in. CSBIMsg */, 0x2000008, 92);
if ( v8 >= 0 )
return 1;
// ...
I tak oto trafiliśmy na rozwiązanie zagadki - czyżby ktoś zapomniał o + 1 rekompensującym poprzednie - 1? Ups.
Rozwiązaniem problemu jest więc inkrementacja obu pól, których wartości są źle wyliczane, przed samym wywołaniem SetConsoleScreenBufferInfoEx:
CONSOLE_SCREEN_BUFFER_INFOEX bi;
bi.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
GetConsoleScreenBufferInfoEx(hcon, &bi);
// Różne operacje na bi.
bi.srWindow.Right += 1;
bi.srWindow.Bottom += 1;
SetConsoleScreenBufferInfoEx(hcon, &bi);
Podsumowując: wywołując parę Get/SetConsoleScreenBufferInfoEx musimy pamiętać o kompensacji wartości w polach srWindow.Right oraz srWindow.Bottom.
I tyle :)
Comments:
Add a comment: