Наскільки я розумію, вся морока із перетворенням SYS.COM в SYS.EXE, а потім виготовлення із нього знову COM-файла, який на льоту сам себе перетворює в EXE, була заради того, щоб підключити printf-подібну функцію, яку можна буде викликати дальнім викликом -- таку, що знаходитиметься в окремому сегменті (при всій умовності сегментів у 8086/8088). Справді, перше, що ми бачимо в "SYS.EXE" -- код такого printf. Перед тим, як перейти до власне коду SYS, розглянемо його.
Використовується ця printf-подібна функція (надалі називатиму просто printf) дещо дивно. Їй, через стек, передається адреса на змінну, що містить адресу стрічки для друку. Тобто -- адреса вказівника на стрічку. Виглядає виклик так:
mov dx, offset NoSystemOnDefDrv_Adr push dx call Printf_sub ;--------------------------------------------- CodeSeg:04CB NoSystemOnDefDrv_Str db 'No system on default drive',0Dh,0Ah,0 CodeSeg:04E8 NoSystemOnDefDrv_Adr dw 4CBh
Але це ще не дивно. Дивно наступне: якщо стрічка формату містить керувальні послідовності ("%-щось-там"), то адреси аргументів повинні знаходитися в пам'яті послідовно, після адреси стрічки формату. Єдиний приклад використання із SYS виглядає так:
mov al, [currentDrive] add al, 40h ; mov byte ptr DriverLetter1_Str, al ; "A"+code mov dx, offset InsertSystemDisk_Adr push dx call Printf_sub ;--------------------------------------------- CodeSeg:04EA InsertSystemDisk_Str db 'Insert system disk in drive %c',0Dh,0Ah CodeSeg:04EA db 'and strike any key when ready',0 CodeSeg:0528 InsertSystemDisk_Adr dw 4EAh CodeSeg:052A DriverLetter1_Adr dw 52Ch CodeSeg:052C DriverLetter1_Str db 'A',0
Нормальні variadic-функції для цього стек використовують...
Перейдемо до розгляду її функціонування. Ось код:
segment PrintfSeg OutFileHandler dw 1 LeftJustify db 0 IsLong db 0 printHexAsLowerLetters db 0 HexLettersTblDispl db 0 DoPrintString db 0 PaddingSize dw 0 NumberBase dw 0 PaddingChar db 20h a0123456789abcd db '0123456789ABCDEFabcdef' temp_IP dw 0 temp_CS dw 0 PrintfOutBuffer db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; 20 EndOfPrintfOutBuffer db 0 db 15h db 0 ; === SUBROUTINE ==================================================== ; Address of the pointer to the string --- in stack. ; IF there are other data, pointers to them are after the pointer to string. ; Attributes: bp-based frame Arg_0 EQU 16h Printf_sub: ; far proc push bp push dx push cx push bx push ax push di push si push es push ds mov bp, sp ; Setup stack frame. push cs pop es ;assume es:PrintfSeg mov di, PrintfOutBuffer ; ofs mov bp, [bp+Arg_0] ; BP = pushed to stack, before the call, value ; (traditionally -- from the DX), ; which contains _address_of_pointer to string ; Important! Next 2-byte words, after the pointer, ; contain pointers to other printf arguments! mov si, [ds:bp+0] ; Now SI --- string address xor bx, bx ; BX --- next variadic argument ; displacement in stack (rel. to BP+2) call PrintfInitForNext NextSymbol: ; ; lodsb cmp al, 25h ; '%' loc_9C3C: jz short DirectiveHandling or al, al jz short ZeroByteMet ; Zero -- C-string terminator call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol jmp short NextSymbol ; =================================================================================== ; Flush buffer at exit ZeroByteMet: ; call DoPrintIfNotEmpty ; DI points to last symbol in buffer. ; If it's address == bufer start address, does nothing. pop ds pop es ; assume es:nothing loc_9C4C: pop si pop di pop ax pop bx pop cx pop dx pop bp ; Take return address from the stack, ; clear agument --- pointer to string address, ; then put return address back to stack. pop word [cs:temp_IP] pop [cs:temp_CS] pop ax push [cs:temp_CS] push word [cs:temp_IP] retf ; =================================================================================== PrintPercentChar: ; call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol jmp short NextSymbol ; =================================================================================== ; http://www.cplusplus.com/reference/cstdio/printf/ DirectiveHandling: ; ; lodsb ; Load char after '%' -- directive code cmp al, '%' jz short PrintPercentChar cmp al, '-' ; Left justify jz short SetLeftJustify cmp al, '+' ; ? jz short ContDirectivesHandling cmp al, 'L' ; Not Long double as in modern C code ; -- just long int jz short SetLong cmp al, 'l' ; long int jz short SetLong cmp al, 30h ; '0' jb short NotDigitDirective cmp al, 39h ; '9' ja short NotDigitDirective cmp al, 30h ; '0' jnz short SetDefPaddingChar0 ; cmp [cs:PaddingSize], 0 jnz short SetDefPaddingChar0 mov byte [cs:PaddingChar], '0' SetDefPaddingChar0: push ax ; We will read paddign size digit by digist, starting from hi. ; So for each next -- mul by 10 and add it mov ax, 10 mul [cs:PaddingSize] mov [cs:PaddingSize], ax pop ax xor ah, ah sub al, 30h ; '0' add [cs:PaddingSize], ax jmp short ContDirectivesHandling ; =================================================================================== SetLeftJustify: ; inc [cs:LeftJustify] jmp short ContDirectivesHandling ; =================================================================================== SetLong: ; ; inc [cs:IsLong] ContDirectivesHandling: ; ; jmp short DirectiveHandling ; =================================================================================== NotDigitDirective: ; ; cmp al, 58h ; 'X' ; Unsigned hexadecimal ; Use upper-case digits (ABCDEF) jz short DoPrintHex2 cmp al, 61h ; 'a' jb short NotSmallChar cmp al, 7Ah ; 'z' jg short NotSmallChar and al, 0DFh ; ToUpper ; Converted directives to Upper NotSmallChar: ; ; cmp al, 58h ; 'X' ; Unsigned hexadecimal ; Was small before "ToUpper". ; So if we here -- use low-case digits, (abcdef) jz short DoPrintHex cmp al, 44h ; 'D' ; Signed decimal integer jz short PrintDecimal cmp al, 43h ; 'C' ; Character jz short PrintChar ; Save ptr to format string cmp al, 53h ; 'S' ; String of characters jz short PrintString call PrintfInitForNext jmp NextSymbol ; =================================================================================== DoPrintHex: ; mov [cs:HexLettersTblDispl], 6 DoPrintHex2: ; mov [cs:NumberBase], 16 jmp short DoPrintInt ; =================================================================================== nop PrintDecimal: ; mov [cs:NumberBase], 10 jmp short DoPrintInt ; =================================================================================== nop PrintString: ; inc [cs:DoPrintString] PrintChar: ; push si ; Save ptr to format string mov si, bx add bx, 2 ; Traveling to each next arg ; Pointer to them are after the pointer to format str. mov si, [ds:bp+si+2] ; Addres of char/string to print cmp [cs:DoPrintString], 0 jnz short PrintingString lodsb cmp al, 0 jz short Exit_PrintCharOrStr call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol jmp short Exit_PrintCharOrStr ; =================================================================================== PrintingString: ; mov cx, [cs:PaddingSize] or cx, cx jz short do_print_str cmp byte [cs:LeftJustify], 0 jnz short do_print_str push si call kindof_strlen_and_pad ; SI -- string adress ; CX --- padding size pop si do_print_str: ; ; push si ; Save ptr to printed string do_next_char_in_str: ; lodsb cmp al, 0 jz short zero_char_met_in_str call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol jmp short do_next_char_in_str ; =================================================================================== zero_char_met_in_str: ; pop si ; Restore ptr to printed string cmp [cs:LeftJustify], 0 jz short Exit_PrintCharOrStr mov cx, [cs:PaddingSize] or cx, cx jz short Exit_PrintCharOrStr call kindof_strlen_and_pad ; SI -- string adress ; CX --- padding size Exit_PrintCharOrStr: ; ; call PrintfInitForNext pop si ; Restore ptr to format string jmp NextSymbol ; Printf_sub endp ; === SUBROUTINE ==================================================== ; SI -- string adress ; CX --- padding size kindof_strlen_and_pad: ; proc near xor dx, dx do_next_char_check: ; lodsb or al, al jz short str_finished inc dx jmp short do_next_char_check ; =================================================================================== str_finished: ; sub cx, dx ; padding symbols jbe short no_padding ; If <0 -- no padding call PerformPadding ; CX --- number of symbols for padding, ; symbol is saved in cs:PaddingChar no_padding: ; retn ; kindof_strlen_and_pad endp ; =================================================================================== DoPrintInt: ; ; push si mov si, bx add bx, 2 ; Traveling to each next arg ; The number is after the pointer to format str. mov ax, [ds:bp+si+2] ; Number to print -- from memory to AX cmp byte [cs:IsLong], 0 jz short notLongInt ; If not long --- upper two bytes, in DX, are zero mov si, bx add bx, 2 ; If printing long --- it is placed in next two bytes mov dx, [ds:bp+si+2] jmp short LongIntPrepared ; =================================================================================== notLongInt: ; xor dx, dx ; If not long --- upper two bytes, in DX, are zero ; Here DX:AX contains high and low part of number LongIntPrepared: ; push bx mov si, [cs:NumberBase] mov cx, [cs:PaddingSize] call ExtractDigits ; Here DX:AX contains high and low part of number ; SI -- base of system ; CX --- padding size ; Recursively calls itself ; On return --- CX=padding left call PerformPadding ; CX --- number of symbols for padding, ; symbol is saved in cs:PaddingChar call PrintfInitForNext pop bx pop si jmp NextSymbol ; === SUBROUTINE ==================================================== ; Here DX:AX contains high and low part of number ; SI -- base of system ; CX --- padding size ; Recursively calls itself ; On return --- CX=padding left ExtractDigits: ; proc near dec cx ; -1 for padding - we have real digit push ax ; Save AX -- lower part mov ax, dx ; AX = upper part xor dx, dx div si ; Div DX:AX pair (DX=0, AX -- upper part of number) by SI -- base ; Result: AX=Quo, DX=Rem ; SI is base, so DX --- obtained digit mov bx, ax ; quotient->BX pop ax ; Restore AX -- lower part div si ; DX(reminder of upper/base):AX(lower) by SI(base) xchg bx, dx ; BX -- rem, DX --- upper/base ; So, BX --- least significant digit in base=SI system push ax ; Save AX=(lower)/SI(base) or ax, dx ; upper/base OR lower/base?.. pop ax ; Restore AX jz short AllDigitsDone push bx call ExtractDigits ; Continue with (DX:AX)/SI --- remains ; (not remainder!!!) to generate next digit pop bx jmp short DoLeftJustify ; AX -- obtainded digit ; =================================================================================== AllDigitsDone: ; cmp [cs:LeftJustify], 0 jnz short DoLeftJustify ; AX -- obtainded digit call PerformPadding ; CX --- number of symbols for padding, ; symbol is saved in cs:PaddingChar DoLeftJustify: ; mov ax, bx ; AX -- obtainded digit cmp al, 10 ; Check if use non-0-9-digits jb short generateDigit cmp [cs:printHexAsLowerLetters], 0 jnz short generateDigit add al, [cs:HexLettersTblDispl] generateDigit: ; mov bx, a0123456789abcd ; "0123456789ABCDEFabcdef" push ds push cs pop ds ; assume ds:PrintfSeg xlatb ; Set AL to memory byte DS:[(E)BX + unsigned AL] ; So, after, AL --- char code for digit, that was in AL pop ds ; assume ds:nothing push cx call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol pop cx retn ; ExtractDigits endp ; === SUBROUTINE ==================================================== ; CX --- number of symbols for padding, ; symbol is saved in cs:PaddingChar PerformPadding: ; proc near or cx, cx jle short locret_9E01 mov al, byte [cs:PaddingChar] NextSymbolP: ; push cx call StoreChar_and_FlushBufferIfFull ; DI --- destination in buffer ; AL --- symbol pop cx loop NextSymbolP locret_9E01: ; retn ; PerformPadding endp ; === SUBROUTINE ==================================================== ; DI --- destination in buffer ; AL --- symbol StoreChar_and_FlushBufferIfFull: ; proc near stosb cmp di, word EndOfPrintfOutBuffer ; word -- to choose correct instruction form; ofs ; it is not optimal, though jz short BufferIsFull locret_9E09: ; retn ; =================================================================================== BufferIsFull: ; mov cx, EndOfPrintfOutBuffer-PrintfOutBuffer ; Buffer size ; StoreChar_and_FlushBufferIfFull endp ; === SUBROUTINE ==================================================== ; Prints fixed-positioned in memory buffer. ; Number of bytes in CX. DoPrintSymbols: ; proc near push bx mov bx, [cs:OutFileHandler] push ds push cs pop ds ; assume ds:PrintfSeg mov dx, PrintfOutBuffer ; ofs mov ah, 40h int 21h ; DOS - 2+ - WRITE TO FILE WITH HANDLE ; BX = file handle, CX = number of bytes to write, DS:DX -> buffer pop ds ; assume ds:nothing pop bx mov di, PrintfOutBuffer ; ofs retn ; DoPrintSymbols endp ; === SUBROUTINE ==================================================== ; DI points to last symbol in buffer. ; If it's address == bufer start address, does nothing. DoPrintIfNotEmpty: ; proc near cmp di, word PrintfOutBuffer ; ofs jz short locret_9E09 sub di, word PrintfOutBuffer ; ofs mov cx, di call DoPrintSymbols ; Prints fixed-positioned in memory buffer. ; Number of bytes in CX. retn ; DoPrintIfNotEmpty endp ; === SUBROUTINE ==================================================== PrintfInitForNext: ; proc near xor ax, ax mov [cs:LeftJustify], al mov [cs:IsLong], al mov [cs:HexLettersTblDispl], al mov [cs:PaddingSize], ax mov byte [cs:PaddingChar], 20h ; ' ' mov [cs:DoPrintString], al retn ; PrintfInitForNext endp ; PrintfSeg ends
Код успішно рекомпілюється за допомогою FASM. Хоча, про рекомпіляцію цілої програми --- див. наступну частину.
На початку функція зберігає багато регістрів, потім, зовсім як доросла, налаштовує кадр стеку. ES налаштовується на сегмент даних функції, в BP кладеться адреса адреси стрічки формату, в SI -- сама адреса стрічки, а в DI --- адреса внутрішнього 20-байтового буфера. Регістр BX використовується для доступу до наступних аргументів функції. Нагадую, вказівники на них мають лежати після вказівника на стрічку формату. Перш ніж почати аналіз стрічки формату, викликається функція, яку я умовно назвав PrintfInitForNext. Вона очищає всі робочі поля, значення яких окремо встановлюються для виводу кожного аргументу: LeftJustify, IsLong, HexLettersTblDispl, PaddingSize, PaddingChar, DoPrintString. Всі, крім PaddingChar, заповнюються нулем, PaddingChar -- пробілом.
Стрічка формату знаходиться в сегменті даних коду, який викликав printf, DS вказує на нього, SI містить зміщення, тому, щоб дістати чергову літеру для аналізу, використовується банальна lodsb. Якщо прочитали початок керувальної послідовності --- символ '%', переходимо до його обробки, інакше -- перевіряємо, чи це не нульовий символ --- ознака кінця стрічки. Якщо так --- виводимо символи, що залишилися в буфері, відновлюємо збережені регістри і виходимо, якщо ні --- додаємо символ до буфера, і, за потреби, спорожнюємо буфер.
Функція, умовно назвав DoPrintIfNotEmpty, перевіряє, чи буфер не порожній (тобто, чи адреса кінця зайнятої частини буфера, яка знаходиться в DI, не рівна адресі його початку). Якщо не порожній, викликає "DoPrintSymbols", передавши їй (в CX) кількість символів у буфері --- DI мінус PrintfOutBuffer.
DoPrintSymbols, яка, власне, друкує, проста --- зберігає необхідні їй регістри, бере із OutFileHandler номер файлу для виводу (в OutFileHandler збережено 1, стандартний вивід), та виводить за допомогою AH=40h/INT 21h (WRITE TO FILE WITH HANDLE).
Функцію, що додає символ до буфера, і, за потреби, спорожнює його, я назвав StoreChar_and_FlushBufferIfFull. Вона, за допомогою stosb (ES:DI якраз вказують на буфер!) кладе символ, і, якщо після цього DI вказує на кінець буфера, зберігає в CX розмір буфера та провалюється до DoPrintSymbols.
Обробка керівних послідовностей (директив)
(Перш ніж продовжувати, варто нагадати собі принципи роботи printf. Див., наприклад, тут: "%[flags][width][.precision][length]specifier")
Як згадувалося вище, коли зустріли '%', переходимо до диспетчеризації директив. Спершу читаємо наступний символ.
Якщо це -- теж '%', друкуємо його, викликавши StoreChar_and_FlushBufferIfFull (символ процента вже в AL) і переходимо до наступного.
Якщо '-' -- встановлюємо вирівнювання відносно лівого краю (inc [cs:LeftJustify], що еквівалентне встановленню в одиницю, бо був нуль) і читаємо наступний символ директиви.
Якщо '+' -- виводити плюс, просто переходимо до наступного символу, нічого не змінюючи. Очевидно, директива ігнорується.
Якщо 'L' чи 'l', на противагу сучасному printf, вважаємо що слід друкувати long int, 4-байтове слово -- встановлюємо IsLong і читаємо наступний символ директиви.
Якщо символ між '0' і '9' -- встановлюємо вирівнювання. (Тут в коді IDA наробила плутанини! Так співпало, що мітка переходу має те саме зміщення відносно команди переходу, що і ширина вирівнювання відносно сегменту даних, тому IDA вжила для них всіх одне ім'я і не дає розділити цих сіамських близнюків різних рас...) Далі, якщо вирівнювання поки рівне нулю --- ще не було прочитано жодної відмінної від 0 цифри вирівнювання, і ми зустріли символ 0, кажемо вирівнювати нулем, а не пробілами. (Якщо я правильно зрозумів, інші символи для вирівнювання не застосовуються, хоча, сучасний printf, здається, так само поводиться...). Якщо ж воно вже було не нульовим або прочитаний символ не '0', тоді вважаємо, що ми прочитали чергову цифру вирівнювання. В такому випадку поточне вирівнювання (в PaddingSize) множиться на 10 і до нього додається прочитана цифра (яка отримується відніманням коду символу '0' від коду символу цифри). Після цього читається наступний символ. Я, правда, не впевнений, як такий код себе поведе, якщо директива буде помилковою, із хаотичною комбінацією цифр та літер...
Якщо ж директива і не цифра --- вважаємо, що це специфікатор, аналізуємо його.
Якщо специфікатор --- 'X', встановлюємо основу системи числення 16, (NumberBase) і переходимо до друку цілого числа (мітка DoPrintInt), інакше перевіряємо, чи специфікатор є малою літерою. Якщо є --- робимо її великою (доволі елегантним чином, "and al, 0DFh" --- див. таблицю ASCII). Якщо знову маємо 'X', значить до перетворення у велику вона була малою. Тоді переходимо до друку у 16-й системі, але з використанням малих літер.
Цікаво вибираються малі та великі літери. В сегменті даних функції є стрічка: "0123456789ABCDEFabcdef". Якщо задано 'X', то символи цифр беруться послідовно, якщо ж 'x', зміщення, для пошуку 16-кових цифр A, B, C, D, E, F, встановлюється в 6 (змінна HexLettersTblDispl), пропускаючи шість великих, та переходячи до малих.
Якщо специфікатор --- 'D' (велике чи мале), друкуємо число як десяткове, встановивши NumberBase=10. Якщо 'C' --- друкуємо одну літеру, якщо 'S' --- C-стрічку.
Інакше просто очищуємо всі налаштування формату (PrintfInitForNext) і переходимо до наступного символу. Тобто, невідомі директиви просто ігноруються.
Отож, наша printf вміє друкувати цілі числа в десятковій та шістнадцятковій системі, літери та С-стрічки. Подивимося як вона це робить.
Друкування літери та стрічки тісно переплетені. (Що за нездорова оптимізація?!)
За міткою PrintString встановлюється в 1 певна змінна, умовно DoPrintString, і провалюється до PrintChar. Тут зберігаємо SI, який містить адресу стрічки формату, в SI кладемо адресу аргументу, до BX додаємо два, якраз розмір ближнього вказівника --- щоб наступний раз звернутися до наступного аргументу. Тоді, за адресою DS:[BP+SI(=попередньому BX)+2] дістаємо адресу аргументу і кладемо в SI. Якщо DoPrintString рівне нулю, значить нам треба надрукувати лише одну літеру. Так як її адреса вже в SI, робимо lodsb, і, якщо це не нуль, виводимо символ за допомогою StoreChar_and_FlushBufferIfFull, чистимо все форматування (PrintfInitForNext), відновлюємо форматування і переходимо до наступного символу. Я так розумію, вирівнювання для однієї літери ігнорується?
Якщо ж друкуємо цілу стрічку, а не одну літеру, то після того, як в DS:SI --- адреса стрічки, переходимо до мітки PrintingString. Ось вивід стрічки з вирівнюванням рахується. Якщо PaddingSize і LeftJustify не дорівнюють нулю, викликається функція, що вставляє потрібну кількість пробілів (kindof_strlen_and_pad). В обох випадках, після --- власне вивід стрічки. Він тривіальний. Завантажити черговий символ. Якщо не нульовий --- передати його StoreChar_and_FlushBufferIfFull для друку. Якщо нульовий --- перевірити, чи слід вирівнювати праворуч. Якщо так --- вивести потрібну кількість символів-заповнювачів, за допомогою тієї ж kindof_strlen_and_pad. Потім, в будь-якому випадку, очистити форматування, відновити SI і перейти до наступного елементу стрічки формату.
Розглянемо саму kindof_strlen_and_pad. Їй в SI передається адреса стрічки, в CX --- ширина поля. Вона підраховує кількість символів у стрічці, (шукаючи нульовий символ), якщо ширина поля менша за стрічку --- не робить нічого, інакше виводить потрібну кількість пробілів чи нулів (CX мінус розмір стрічки) за допомогою функції PerformPadding. Остання просто бере символ з PaddingChar і CX раз виводить його за допомогою StoreChar_and_FlushBufferIfFull.
Із друком стрічки --- все. Залишився друк числа. Починається він із того ж фокусу із діставанням аргумента за вказівником, збереженим десь після вказівника на стрічку формату (див. вище для стрічок та літер). Витягнуте число кладеться в AX ("mov ax, ds:[bp+si+2]"). Якщо в стрічці формату вибрано long, наступних два байти беремо із адреси в наступному параметрі, і кладемо в DX. Інакше в DX кладемо нуль. Далі викликається ExtractDigits, яка отримує в DX:AX число, основу системи числення в SI, вирівнювання в CX. Після повернення в CX буде ширина вирівнювання, що залишилася. Вона передається PerformPadding, для завершення справи із вирівнюванням. Після цього чистимо форматування (PrintfInitForNext), відновлюємо регістри та переходимо до наступного символу.
ExtractDigits зменшує CX на 1 -- одну цифру точно знайшли, зберігає молодшу частину, AX, старшу, із DX, ділить на основу, тоді "остачу від ділення (в DX)":"молодшу частину" знову ділить на основу. Остача -- власне цифра у вибраный системы числення. Далі відновлюється конфігурація, коли частка від ділення на основу --- в DX:AX. (Ех, тяжко із 16-бітними регістрами, мовчу вже про епоху 8-бітних. :), якщо вона не рівна нулю, знайдена цифра зберігається в стек, та знову викликає ExtractDigits --- для продовження виколупування цифр. Якщо ж залишок рівний нулю -- перевіряємо, чи вирівнювання по лівому краю, якщо так --- переходимо до виводу (використовується хитрий трюк на основі того, що cmp робить sub, і відкидає результат, лише модифікуючи прапорці, тому для "cmp [cs:LeftJustify], 0" ZF=1 якщо перший аргумент не нуль, ), туди ж потрапляємо після повернення із рекурисвного функції, відновивши перед тим цифру в BX, якщо ж ні, не вирівнювати по лівому краю, тобто --- вирівнювати по правому, виконуємо вирівнювання і переходимо все до того ж виводу реальних символів. Зауважте, що рекурсивний виклик забезпечить правильний порядок символів --- від наймолодшої праворуч, до найстаршої ліворуч. (Ділення дає їх в зворотному порядку).
Для шістнадцяткових цифр враховується, використовувати великі чи малі літери для A-F. Якщо малі, додається відповідне зміщення (яке згадувалося вище --- HexLettersTblDispl). Тоді, за допомогою xlat цифрі ставиться у відповідність символ із таблички 0123456789ABCDEFabcdef. Отриманий символ виводиться за допомогою StoreChar_and_FlushBufferIfFull. (Всілякі ігрища з регістрами, для їх збереження-відновлення опущені, див. код).
Все, із функціонуванням розібралися.
Псевдо-документація
Функція, хоч і printf-подібна, але дивна.- Адреса адреси стрічки формату знаходиться в стеку, як для нормальної функції.
- Адреси наступних аргументів повинні знаходитися в пам'яті зразу після адреси стрічки. Зокрема, якщо я не наплутав чого, адреса 32-бітного long передається двічі -- для молодшого слова і для старшого.
- Розуміє наступні "директиви": %% --- вивести '%'; %x та %X --- вивести шістнадцяткове число, великими чи малими літерами, відповідно; %d та %D --- вивести десяткове число, %c чи %C --- одну літеру, %s чи %S --- вивести C-стрічку, що закінчується нулем. Крім X --- байдуже, велика літера використовується, чи мала. Невідомі директиви ігноруються.
- Розуміє наступні модифікатори: '-' --- вирівнювати по лівому краю (по замовчуванню -- по правому), + --- ігнорує, хоча і знає про нього, L чи l --- виводити 32-бітний long int. Вирівнювання задається десятковим числом, якщо перший символ нуль -- вирівнює нулем, інакше --- пробілами. (Не перевіряв, але, здається, вони можуть йти чи не в довільному порядку, поки не зустрінеться директива --- символ, відмінний від цифри, +, -, %, L чи l.)
Походження
Стало мені цікаво, звідки такий куций printf взявся. Вихід PC-DOS 3.00 -- це друга половина (серпень) 1984 року, значить відповідний засіб мав існувати до того. Першим припущенням було, що він взятий із стандартної бібліотеки C якогось із компіляторів, з яким працював Microsoft. Стаття про Microsoft C на вікі, стверджує, що Microsoft C 1.0, який базувався на Lattice C, (точніше, був його перепакованим варіантом) , вийшов в 1983. Про C 3.0 там говориться, "It was being used inside Microsoft (for Windows and Xenix development) in early 1984", Хоча як окремий продукт, випущено вже в 1985, тому всі версії між 1.0 і 3.0 могли б мати відношення. Крім того, "vanilla" Lattice C, який виходив з 1982, теж не варто викидати зі списку.Тому, я зібрав по різних сайтах, присвячених старому софту їх версії тих часів, та проглянув, як вони реалізовують printf. Проглядалося двома способами. Там, де IDA змогла зрозуміти LIB-файли, дизасемблював їх безпосередньо. Крім того, компілював мінімальний приклад із printf, дизасемблюючи і його. Досліджувалися наступні компілятори (ну, які вдалося знайти): Lattice C 2.12, 1982 року, Lattice C 3.0, Microsoft C 2.03, Microsoft C 3.00. Результат виявився несподіваним. Нічого схожого. Абсолютно. Їхні printf значно просунутіші, ближчі до сучасних. (Інша справа, що такі SYS не потрібні, але йому, по великому рахунку, printf ВЗАГАЛІ не потрібен.) Не вдалося знайти й інших функцій, з іншою (не printf) назвою, які виглядали б схоже. Тут гарантії не дам --- тотальної перевірки не робив, то б зайняло місяці, але з високою ймовірністю -- їх немає.
Тобто, ймовірно, ця функція не походить із якогось (відомого мені) компіляторного пакету.
Спробував я шукати і з протилежного боку. Найближче, що знайшов, трапилося ось у цьому документі: "IBM XENIX Programmers Guide To Library Functions" (та в інших схожих документах на XENIX), сторінка 2-13, 36-та у файлі:
Тут перераховано якраз формати, які трапляються у нашої жертви, правда, плюс %o. Однак, більш повні довідники (конкретно того, до якого відсилається цей документ, знайти не вдалося), навіть "Microsoft XENIX Programmers Manual" 1979-го року (!), описують (стор. 316-317 у файлі), як мінімум, формати %f, %g, %e для float i double.
Так що, якщо я чогось жорстоко не прогледів, походження цього загадкового printf відслідкувати не вдалося. Можливо навіть -- він був написаний спеціально. Ну, не для SYS.COM виключно, але для отого комплекту системних утиліток DOS.
Наступна частина, нарешті, перейде до самої SYS.
Немає коментарів:
Дописати коментар