No hace mucho que os hablaba de Uptime para DOS, y que publiqué la versión 1.00 y la 1.10, un sencillo programa escrito en FAST, y que replicaba el comando uptime de UNIX.
El desarrollo empezó muy bien, con una eficiencia casi de ensamblador, pero contando con la ventaja de las estructuras de alto nivel. Hasta que me topé con el impedimento de las conversiones entre números de 16 y 32 bit, y con el maldito bug de mod32.
Entonces me planteé escribir la versión 2.00 en ensamblador. Menos de 150 lineas en código FAST, no parecían gran cosa en ensamblador. Así que comencé con JWASM (Japheth’s Watcom Assembler), un ensamblador y enlazador al mismo tiempo, compatible con MASM, de código abierto y que se contruyó sobre la base de Open Watcom Assembler (WASM / OWASM).
Durante el desarrollo descubrí UASM antes HASM y antes HJWASM, un fork de JWASM que había quedado abandonado, y que me inspiró nuevas motiviaciones a la hora de programar. También tuve ocasión de probar ASMC, otro fork paralelo también de JWASM.
Al final, esas menos de 150 lineas, se convirtieron en casi 500 lineas de ensamblador en Uptime 2.10, incluyendo funciones genéricas como la impresión de caracteres, de cadenas, o la conversión de BCD a decimal. Finalmente, esas 500 lineas de assembly, resultaron ser el programa más largo que había hecho a tan bajo nivel, desde hace casi 20 años, y aunque tuve que reaprender, fue curioso ver como hay cosas que nunca se olvidan.
A pesar de ello, el ejecutable resultante, se redujo de 2.005 bytes en FAST a 1.310 bytes con ensamblador, todo ello a pesar de las nuevas características que aproveché para añadir:
– Nueva opción -r para forzar la inicialización.
– Ayuda más completa.
– Comprobación de CMOS mejorada.
Finalmente, quedó un pequeño programita, con unos requisitos mínimos, que hoy en día os parecerían increíbles, y que lo harían funcionar en un PC de 1983 sin problemas:
– Sistema operativo compatible MS-DOS 2.0 o superior.
– CPU 8088 o superior.
– 3 Kb. de espacio libre en disco/disquette.
– 4 Kb. de memoria convencional disponible.
Como de costumbre, tenéis tanto en la página oficial como en Sourceforge el código fuente completo y los ejecutables, pero por lo que pudiera pasar, lo copio también aquí (5 Kb. en formato ZIP). Para aquellos que tengáis curiosidad de la pinta que tiene el código FAST, lo pongo completo a continuación:
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
.model tiny
.stack 64
.code
org 100h
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
.startup
;Parse command-line options
mov ax, word ptr ds:[82h]
.switch ax
.case 'h-', 'H-', '?-', 'h/', 'H/', '?/'
call ShowHelp
.endc
.case 'r-', 'R-', 'r/', 'R/'
mov dx, offset acReset
call PrintText
call DoReset
.endc
.default
call DoIt
.endsw
.exit
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
DoIt proc
;Print current time
call GetCurrentTime
call GetAlarmTime
;If last time is zero, force automatic reset
;386
if @cpu and 1000b
mov eax, dword ptr [lSecondsA]
.if (eax == 0)
call DoReset
.endif
else
mov ax, word ptr [lSecondsA]
mov dx, word ptr [lSecondsA+2]
.if (ax == 0)
.if (dx == 0)
call DoReset
.endif
.endif
endif
;386
if @cpu and 1000b
movzx ax, iHour
else
xor ah, ah
mov al, iHour
endif
;2 digits
mov si, 2
call PrintNumber0
mov al, ':'
call PrintChar
;386
if @cpu and 1000b
movzx ax, iMinute
else
xor ah, ah
mov al, iMinute
endif
mov si, 2
call PrintNumber0
mov al, ':'
call PrintChar
;386
if @cpu and 1000b
movzx ax, iSecond
else
xor ah, ah
mov al, iSecond
endif
mov si, 2
call PrintNumber0
mov dx, offset acStart
call PrintText
;Substract seconds from startup seconds
;386
if @cpu and 1000b
mov eax, dword ptr [lSeconds]
mov ebx, dword ptr [lSecondsA]
sub eax, ebx
xor edx, edx
mov ebx, 3600
div ebx
else
mov ax, word ptr [lSeconds]
mov dx, word ptr [lSeconds+2]
mov bx, word ptr [lSecondsA]
mov cx, word ptr [lSecondsA+2]
sub ax, bx
sbb dx, cx
mov bx, 3600
div bx
endif
;mov word ptr [lSecondsE], ax
;mov word ptr [lSecondsE+2], dx
;mov ax, word ptr [lSecondsE]
;mov dx, word ptr [lSecondsE+2]
;Print hours elapsed
call PrintNumber
mov ax, dx
mov dx, offset acHour
call PrintText
;Print minutes elapsed
xor dx, dx
mov bx, 60
div bx
call PrintNumber
mov ax, dx
mov dx, offset acMinute
call PrintText
;Print seconds elapsed
call PrintNumber
mov dx, offset acSecond
call PrintText
;Return
xor al, al
ret
DoIt endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
DoReset proc uses ax
xor al, al
out 70h, al
in al, 71h
xchg ah, al
mov al, 1
out 70h, al
xchg ah, al
out 71h, al
;Check CMOS is present
xchg ah, al
mov al, 1
out 70h, al
in al, 71h
.if (al != ah)
mov dx, offset acNoCMOS
call PrintText
.exit
.endif
mov al, 2
out 70h, al
in al, 71h
xchg ah, al
mov al, 3
out 70h, al
xchg ah, al
out 71h, al
mov al, 4
out 70h, al
in al, 71h
xchg ah, al
mov al, 5
out 70h, al
xchg ah, al
out 71h, al
call GetAlarmTime
mov al, 1
ret
DoReset endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
ShowHelp proc
mov dx, offset acCopyright
call PrintText
call ReadChar
mov dx, offset acCrLf
call PrintText
;Return
mov al, -1
ret
ShowHelp endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;386
if @cpu and 1000b
GetCurrentTime proc uses eax
else
GetCurrentTime proc uses ax bx dx
endif
xor al, al
out 70h, al
in al, 71h
call BCD2Bin
mov iSecond, al
mov al, 2
out 70h, al
in al, 71h
call BCD2Bin
mov iMinute, al
mov al, 4
out 70h, al
in al, 71h
call BCD2Bin
.if (al > 81h)
sub al, 75h
.endif
mov iHour, al
;Fill lSeconds with total number of seconds
;386
if @cpu and 1000b
movzx ax, iSecond
else
xor ah, ah
mov al, iSecond
endif
mov word ptr [lSeconds], ax
;386
if @cpu and 1000b
movzx ax, iMinute
else
xor ah, ah
mov al, iMinute
endif
;386
if @cpu and 1000b
imul ax, 60
else
mov bx, 60
mul bx
endif
add word ptr [lSeconds], ax
;386
if @cpu and 1000b
movzx eax, iHour
imul eax, 3600
add dword ptr [lSeconds], eax
else
xor ah, ah
mov al, iHour
mov bx, 3600
mul bx
add word ptr [lSeconds], ax
adc word ptr [lSeconds+2], dx
endif
ret
GetCurrentTime endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
if @cpu and 1000b
GetAlarmTime proc uses eax
else
GetAlarmTime proc uses ax bx dx
endif
mov al, 1
out 70h, al
in al, 71h
call BCD2Bin
mov iSecondA, al
mov al, 3
out 70h, al
in al, 71h
call BCD2Bin
mov iMinuteA, al
mov al, 5
out 70h, al
in al, 71h
call BCD2Bin
.if (al > 81h)
sub al, 75h
.endif
mov iHourA, al
;Fill lSeconds with total number of seconds
;386
if @cpu and 1000b
movzx ax, iSecondA
else
xor ah, ah
mov al, iSecondA
endif
mov word ptr [lSecondsA], ax
;386
if @cpu and 1000b
movzx ax, iMinuteA
else
xor ah, ah
mov al, iMinuteA
endif
;386
if @cpu and 1000b
imul ax, 60
else
mov bx, 60
mul bx
endif
add word ptr [lSecondsA], ax
;386
if @cpu and 1000b
movzx ax, iHourA
imul eax, 3600
add dword ptr [lSecondsA], eax
else
xor ah, ah
mov al, iHourA
mov bx, 3600
mul bx
add word ptr [lSecondsA], ax
adc word ptr [lSecondsA+2], dx
endif
ret
GetAlarmTime endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Number in AL
BCD2Bin proc uses bx
; ( (bcd & 0xF0) >> 1) + ( (bcd & 0xF0) >> 3) + (bcd & 0xf)].
; (bcd / 16) * 10) + (bcd & 0xf)
mov ah, al
and al, 0f0h
shr al, 1
mov bl, al
mov al, ah
and al, 0f0h
;186
if @cpu and 10b
shr al, 3
else
rept 3
shr al, 1
endm
endif
add bl, al
mov al, ah
and al, 0fh
add bl, al
mov al, bl
ret
BCD2Bin endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Text in DX
PrintText proc uses ax
mov ah, 9
int 21h
ret
PrintText endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Returns char in AL
ReadChar proc
mov ah, 8
int 21h
ret
ReadChar endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Char in AL
PrintChar proc uses dx
mov ah, 2
mov dl, al
int 21h
ret
PrintChar endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Number in AX
PrintNumber proc uses ax bx cx dx
xor cx, cx
mov bx, 10
.repeat
xor dx, dx
div bx ;divide by ten
push ax
add dl, '0' ;convert dl to ascii
pop ax ;restore ax
push dx ;digits are in reversed order, must use stack
inc cx ;remember how many digits we pushed to stack
test ax, ax ;if ax is zero, we can quit
.until zero? ;jnz
;cx is already set
mov ah, 2 ;2 is the function number of output char in the DOS Services.
PrintNumberLoop:
pop dx ;restore digits from last to first
int 21h ;calls DOS Services
loop PrintNumberLoop
ret
PrintNumber endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
;Number in AX, Digits in SI
PrintNumber0 proc uses ax bx cx dx
xor cx, cx
mov bx, 10
.repeat
xor dx, dx
div bx ;divide by ten
push ax
add dl, '0' ;convert dl to ascii
pop ax ;restore ax
push dx ;digits are in reversed order, must use stack
inc cx ;remember how many digits we pushed to stack
test ax, ax ;if ax is zero, we can quit
.until zero? ;jnz
;cx is already set
;mov ah, 2 ;2 is the function number of output char in the DOS Services.
push cx
sub si, cx
.if (si > 0)
mov cx, si
mov dl, '0'
mov ah, 2
PrintNumber0Loop0:
int 21h ;fast DOS output
loop PrintNumber0Loop0
.endif
pop cx
mov ah, 2
PrintNumber0Loop:
pop dx ;restore digits from last to first
int 21h ;calls DOS Services
loop PrintNumber0Loop
ret
PrintNumber0 endp
;--------------------------------------------------------------------------------------------------------------------------------------------------------------
.data
lSeconds dd ?
lSecondsA dd ?
;lSecondsE dd ?
;iYear dw ?
;iMonth db ?
;iDay db ?
iHour db ?
iMinute db ?
iSecond db ?
iYearA dw ?
iMonthA db ?
iDayA db ?
iHourA db ?
iMinuteA db ?
iSecondA db ?
acStart db " up " , '$'
acHour db " hour, " , '$'
acMinute db " minute, " , '$'
acSecond db " second, "
acEnd db "1 user, load average: 0.00, 0.00, 0.00", 13, 10, '$'
acCrLf db 13, 10, '$'
acReset db "Uptime counter reseted to zero.", 13, 10, '$'
acNoCMOS db "No CMOS available to write.", 13, 10, '$'
acCopyright db 13, 10
db "UPTIME R2.60 (c) 2017 by Javier Gutierrez Chamorro (Guti)", 13, 10
db "Display system uptime under DOS", 13, 10, 10
acHelp db "UPTIME displays DOS uptime, by automatically detecting when it was firstly", 13, 10
db "booted, mimicing its UNIX counterparts.", 13, 10, 10
db "Syntax is: UPTIME [-h|-r]", 13, 10, 10
db "Examples:", 13, 10, 10
db " UPTIME -h", 13, 10
db " Shows this help screen.", 13, 10, 10
db " UPTIME -r", 13, 10
db " Forces reseting the counter. Useful if it is not automatically detected", 13, 10
db " properly during startup.", 13, 10, 10
db "More information at:", 13, 10
db " https://nikkhokkho.sourceforge.io/static.php?page=UPTIME", 13, 10
db "Press ENTER to continue..."
db '$'
endd
mod call call mod mod call call mod… Me mareo. Currazo que te has pegado Guti, me quito el sombrero.
Gracias bianamaran. Es un trabajo más sencillo de lo que parece una vez tienes cierta soltura. Eso sí, utilidad práctica muy poca, pero reconozco que he disfrutado mucho programándolo y puliéndolo.
Date cuenta, que se utilizan ciertas construcciones de alto nivel: .if, .while, .case… ¡Imagina la pinta que habría tenido sin ellas!
La gente que programais ensamblador siempre me habeis parecido dioses.
Yo, que me considero amante de la programación, lo intenté, pero al final me quedé en Clipper (dBase) (¡qué tiempos!), un poco de C, Python y al final, por comodidad, un script kiddie en Dash (sh), que para mis nesecidades, es lo que mejor me viene.
¡Pá lo que hemos quedao!
Supongo que a mi el ensamblador no me costó demasiado, porque había hecho algunos pinitos con el Spectrum. En realidad, en la época PC, fue el lenguaje ensamblador el que me ayudó a entender los punteros de C y C++. Clipper me encantaba, era un lenguaje que te daba una productividad altísima.
En 5 lineas de código, montabas una pantalla con un formulario, y con 5 más, conseguías que se guardase, en base de datos. Eso por no hablar de los archivos indexados, que en aquella época, eran algo fuera de lo común.
Eso es algo que no he comprendido jamás, el tema de los punteros de C.
Una variable que apunta al contenido de otra.
Y la explicación que te da todo el mundo es que es lo mejor que tiene C, que si es optimizacición de memoria y demás, pero nunca supe la utilidad real.
Supongo que me falta base de bajo nivel.
En alguno de sus sentidos prácticos, te resultará más sencillo de comprender Ender Wiggin. Cuando en un lenguaje de programación, pasas un parámetro por referencia, lo que estás haciendo, es pasar un puntero a ese parámetro, lo que permite que la función lo modifique.