Uptime 2 para DOS

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.

Uptime 2 para DOS

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

6 comentarios en “Uptime 2 para DOS”

  1. Javier Gutiérrez Chamorro (Guti)

    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!

  2. 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!

  3. Javier Gutiérrez Chamorro (Guti)

    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.

  4. 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.

  5. Javier Gutiérrez Chamorro (Guti)

    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.

Deja un comentario