неділю, 22 грудня 2013 р.

Аналіз SYS.COM з PC-DOS 3.00 -- частина друга, printf

Наскільки я розумію, вся морока із перетворенням 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.)
Ніби все. SYS використовує її 6 раз, один із яких -- щоб вивести вказаний в стірчці формату char. Одну літеру, тобто. Але, підозрюю, вся возня із EXE-COM --- заради неї...

Походження

Стало мені цікаво, звідки такий куций 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.

Немає коментарів:

Дописати коментар