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.
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.
By the way...
If you'd like to learn SSH in depth, in the second half of January'25 we're running a 6h course - you can find the details at hexarcana.ch/workshops/ssh-course
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: