Be a Time Lord
The standard C libraries contain a pretty substantial group of functions that manipulate dates and times. Although these functions were originally designed to handle date values generated by the real-time clock in ancient AT&T minicomputer hardware, they have by now become a standard interface to any operating system's real-time clock support. People who program in C under DOS or for Windows use the very same group of functions, and they work more or less the same way irrespective of what platform you're working with.By understanding how to call these functions as assembly language procedures, you'll be able to read the current date, express time and date values in numerous formats, apply timestamps to files, and do many other useful things.Let's take a look at how it works.
The C Library's Time Machine
Somewhere deep inside the standard C library, there is a block of code that, when invoked, looks at the real-time clock in the computer, reads the current date and time, and translates that into a standard, 32-bit unsigned integer value. This value is the number of seconds that have passed in the "Unix Epoch," which began on January 1, 1970, 00:00:00 universal time. Every second that passes adds one to this value. When you read the current time or date via the C library, what you'll retrieve is the current value of this number.The number is called time_t. The time_t value is currently in the high 900,000,000s, and will flip to 10 digits (1 billion seconds since January 1, 1970) on September 9, 2001, at 7:46:40 A.M. UTC. This isn't a Y2K-style hazard in the immediate future, since even a signed 32-bit integer can express a quantity over 2 billion, and an unsigned 32-bit integer can express over 4 billion. Furthermore, a properly implemented C library doesn't assume that this is a 32-bit quantity at all. So, when the whole thing flips in the year 2069, we'll already be using at least 64-bit values for everything and the whole problem will be put off for another 292 billion years or so. If we haven't fixed it once and for all by then, we'll deserve to go down in the Cosmic Crunch that cosmologists are predicting.A time_t value is just an arbitrary seconds count and doesn't tell you much on its own, though it can be useful for calculating elapsed times in seconds. A second standard data type implemented by the standard C library is much more useful. A tm structure (which is often called a struct, and which is what Pascal people would call a record) is a grouping of nine 32-bit values that express the current time and date in separately useful chunks, as summarized in Table 13.3. Note that although a struct (or record) is nominally a grouping of unlike values, in the current x86 Linux implementation, a tm value is more like an array or a data table, because all nine elements are the same size, which is 32 bits, or 4 bytes. I've described it that way in Table 13.3, by including a value that is the offset from the beginning of the structure for each element in the structure. This allows you to use a pointer to the beginning of the structure and an offset from the beginning to close in on any given element of the structure.
OFFSET IN BYTES | C LIBRARY NAME | DEFINITION |
---|---|---|
0 | tm_sec | Seconds after the minute, from 0 |
4 | tm_min | Minutes after the hour, from 0 |
8 | tm_hour | Hour of the day, from 0 |
12 | tm_mday | Day of the month, from 1 |
16 | tm_mon | Month of the year, from 0 |
20 | tm_year | Year since 1900, from 0 |
24 | tm_wday | Days since Sunday, from 0 |
28 | tm_yday | Day of the year, from 0 |
32 | tm_isdst | Daylight Savings Time flag |
There are C library functions that convert time_t values to tm values and back. I cover a few of them in this book, but they're all pretty straightforward, and once you've thoroughly internalized the C calling conventions, you should be able to work out an assembly calling mechanism for any of them.
Fetching time_t Values from the System Clock
Any single second of time (at least those seconds after January 1, 1970) can be represented as a 32-bit unsigned integer in the Unix system.
Fetching the value for the current time is done by calling the time function:
push dword 0 ; Push a 32-bit null pointer to stack, since
; we don't need a buffer. Time value is
; returned in eax.
call time ; Returns calendar time in eax
add esp, byte 4 ; Clean up stack after call
mov [oldtime],eax ; Save time value in memory variable
The time function can potentially return the time value in two places: In EAX, or in a buffer that you allocate somewhere. To have time place the value in a buffer, you pass it a pointer to that buffer on the stack. If you don't want to store the time value in a buffer, you must still hand it a null pointer on the stack. That's why we push a 0 value in the preceding code; 0 is the value of a null pointer.No other arguments need to be passed to time. On return, you'll have the current time value (what Unixoids call time_t) in EAX. That's all there is to it.
Converting a time_t Value to a Formatted String
At this writing, time_t is up to about 950,000,000. (Scary to think that that many seconds have passed since the middle of my senior year in high school-which is precisely the time I first learned about computers!) By itself, time_t doesn't tell you a great deal. The C library contains a function that will return a pointer to a formatted string representation of a given time_t. This is the ctime function. It returns a pointer to a string buried somewhere in the runtime library. This string has the following format:
Thu Dec 2 13:59:20 1999
The first field is a three-character code for the day of the week, followed by a three-character code for the month and a two-space field for the day of the month. The time follows, in 24-hour format, and the year brings up the rear. For good measure (though it is sometimes a nuisance), the string is terminated by a newline.Here's how you use ctime:
push dword oldtime ; Push *address* of calendar time value
call ctime ; Returns pointer to ASCII time string in eax
add esp, byte 4 ; Clean up stack after call
This looks pretty conventional, but there is something here that you must notice, as it's a little unconventional: You pass ctime the address of a time_t value, not the value itself! You're used to passing 32-bit integer values by pushing the values themselves onto the stack, say, for display by printf. Not so here. A time_t value is currently, under Linux, represented as a 4-byte integer, but there is no promise that it will always be thus. So, to keep its options open (and to ensure that Unix can be used for thousands or even millions of years to come, egad), the C library requires a pointer to the current time. Maybe in a thousand years it'll be a quad word . . . who's to say?So you push a pointer to the time_t value that you want to represent as a string, and then call ctime. What ctime returns is a pointer to the string, which it keeps somewhere inside the library. You can use that pointer to display the string on the screen via printf or to write it to a text file.
Generating Separate Local Time Values
The C library also gives you a function to break out the various components of the date and time into separate values, so you can use them separately or in various combinations. This function is localtime, and given a time_t value, it will break out the date and time into the fields of a tm structure described in Table 13.3. Here's the code to call it:
push dword oldtime ; Push address of calendar time value
call localtime ; Returns pointer to static time structure in eax
add esp, byte 4 ; Clean up stack after call
Here, oldtime is a time_t value. Given this value, localtime returns in EAX-much in the fashion of ctime-a pointer to a tm structure within the runtime library somewhere. By using this pointer as a base address, you can access the fields in the structure by using a constant displacement from the base (here, shown as stored in EAX):
mov edx, dword [eax+20] ; Year value is 20 bytes offset into tm
push edx ; Push value onto the stack
push dword yrmsg ; Push address of the base string
call printf ; Display string and year value with printf
add esp, byte 8 ; Clean up the stack
By using the displacements shown in Table 13.3, you can access all the other components of the time and the date in the tm structure, stored as 32-bit integer values.
Uninitialized Storage and [.bss]
To newcomers, the difference between the [.data] and [.bss] sections of the program may be obscure. Both are used for holding variables . . . so, what's the deal? Is it (like many other things in computing) just more, . . . um, . . . bss?Not really. Again, the difference is more a matter of convention than anything else. The [.data] section was intended to contain initialized data; that is, variables that you provide with initial values. Most of the time, these will be base strings for data display containing prompts and other string data that doesn't change during the course of a program's execution. Sometimes you'll store count values there that define the number of lines in an output report, and so on. These values are much like values defined as CONSTANT in Pascal. They're defined at compile time and are not supposed to change.In assembly, of course, you can change them if you want. But for variables that begin without values (that is, are uninitialized) which are given values over the course of a program's execution (which is the way most high-level language programmers think of variables), you should probably allocate them in the [.bss] section.There are two groups of data-definition pseudoinstructions that I've used informally all along. They are what I call the defines and the reserves. The define pseudoinstructions give a name, a size, and a value to a data item. The reserves only give a name and a size. Here are some examples:
rowcount dd 6
fileop db 'w',0
timemsg db "Hey, what time is it? It's %s",10,0
timediff resd 1 ; Reserve 1 integer (4 bytes) for time difference
timestr resb 40 ; Reserve 40 bytes for time string
tmcopy resd 9 ; Reserve 9 integer fields for time struct tm
The first group are the defines. The ones you'll use most often are DD (define double) and DB (define byte). The DB pseudoinstruction is unique in that it allows you to define character arrays very easily, and it is generally used for string constants. For more advanced work, NASM provides you with DW (define word) for 16-bit quantities, DQ (define quad word) for 64-byte quantities, and DT (define ten-byte) for 80-bit quantities. These larger types are used for floating-point arithmetic, which I won't be covering in this book.The second group are reserves. They all begin with "RES," followed by the code that indicates the size of the item to be reserved. NASM defines RESB, RESW, RESD, RESQ, and REST for bytes, words, doubles, quads, and 10-bytes. The reserves allow you to allocate arrays of any type, by specifying an integer constant after the pseudoinstruction. RESB 40 allocates 40 bytes, and RESD 9 allocates 9 doubles (32-bit quantities) all in a contiguous array.
Making a Copy of clib's tm Struct with MOVSD
It's sometimes handy to be able to keep a separate copy of a tm structure, especially if you're working with several date/time values at once. So, after you use localtime to fill the C library's hidden tm structure with date/time values, you can copy that structure to a structure allocated in the [.bss] section of your program.Doing such a copy is a straightforward use of the REP MOVSD (Repeat Move String Double) instruction. MOVSD is an almost magical thing: Once you set up pointers to the data area you want to copy, and the place you want to copy it to, you store the size of the area in ECX and let REP MOVSD do the rest. In one operation it will copy an entire buffer from one place in memory to another.
To use REP MOVSD, you place the address of the source data-that is, the data to be copied-into ESI. You move the address of the destination location-where the data is to be placed-in EDI. The number of items to be moved is placed in ECX. You make sure the Direction flag is cleared (for more on this, see Chapter 11) and then execute REP MOVSD:
mov esi, eax ; Copy address of static tm from eax to esi
mov edi, dword tmcopy ; Put the address of the local tm copy in edi
mov ecx,9 ; A tm struct is 9 dwords in size under Linux
cld ; Clear df to 0 so we move up-memory
rep movsd ; Copy static tm struct to local copy in .bss
Here, we're moving the C library's tm structure to a buffer allocated in the [.bss] section of the program. The tm structure is 9 double words-36 bytes-in size. So, we have to reserve that much space and give it a name:
tmcopy resd 9 ; Reserve 9 integer fields for time struct tm
The preceding code assumes that the address of the C library's already-filled tm structure is in EAX, and that a tm structure tmcopy has been allocated. Once executed, it will copy all of the tm data from its hidey-hole inside the C runtime library to your freshly allocated buffer.The REP prefix puts MOVSD in automatic-rifle mode, as I explained in Chapter 11. That is, MOVSD will keep moving data from the address in ESI to the address in EDI, counting ECX down by one with each move, until ECX goes to zero. Then it stops.One oft-made mistake is forgetting that the count in ECX is the count of data items to be moved, not the number of bytes to be moved! By virtue of the D on the end of its mnemonic, MOVSD moves double words, and the value you place in ECX must be the number of 4-byte items to be moved. So, in moving 9 double words, MOVSD actually transports 36 bytes from one location to another-but you're counting doubles here, not bytes.The following program knits all of these snippets together into a demo of the major Unix time features. There are many more time functions to be studied in the C library, and with what you now know about C function calls, you should be able to work any of them out.
; Source name : TIMETEST.ASM
; Executable name : TIMETEST
; Version : 1.0
; Created date : 12/2/1999
; Last update : 12/3/1999
; Author : Jeff Duntemann
; Description : A demo of time-related functions for Linux, using NASM 0.98
;
; Build using these commands:
; nasm -f elf timetest.asm
; gcc timetest.o -o timetest
;
[SECTION .text] ; Section containing code
extern ctime
extern getchar
extern printf
extern localtime
extern time
global main ; Required so linker can find entry point
main:
push ebp ; Set up stack frame for debugger
mov ebp,esp
push ebx ; Program must preserve ebp, ebx, esi, & edi
push esi
push edi
;;; Everything before this is boilerplate; use it for all ordinary apps!
;;; Generate a time_t calendar time value with clib's time function============
push dword 0 ; Push a 32-bit null pointer to stack, since
; we don't need a buffer. Time value is
; returned in eax.
call time ; Returns calendar time in eax
add esp, byte 4 ; Clean up stack after call
mov [oldtime],eax ; Save time value in memory variable
;;; Generate a string summary of local time with clib's ctime function=========
push dword oldtime ; Push address of calendar time value
call ctime ; Returns pointer to ASCII time string in eax
add esp, byte 4 ; Clean up stack after call
push eax ; Push pointer to ASCII time string on stack
push dword timemsg ; Push pointer to base message text string
call printf ; Merge and display the two strings
add esp, byte 8 ; Clean up stack after call
;;; Generate local time values into clib's static tm struct====================
push dword oldtime ; Push address of calendar time value
call localtime ; Returns pointer to static time structure in eax
add esp, byte 4 ; Clean up stack after call
;;; Make a local copy of clib's static tm struct===============================
mov esi, eax ; Copy address of static tm from eax to esi
mov edi, dword tmcopy ; Put the address of the local tm copy in edi
mov ecx,9 ; A tm struct is 9 dwords in size under Linux
cld ; Clear df to 0 so we move up-memory
rep movsd ; Copy static tm struct to local copy in .bss
;;; Display one of the fields in the tm structure==============================
mov edx, dword [tmcopy+20] ; Year value is 20 bytes offset into tm
push edx ; Push value onto the stack
push dword yrmsg ; Push address of the base string
call printf ; Display string and year value with printf
add esp, byte 8 ; Clean up the stack
;;; Wait a few seconds for user to press Enter so we have a time difference====
call getchar
;;; Calculating seconds passed since program began running with difftime=======
push dword 0 ; Push null ptr; we'll take value in eax
call time ; Get current time value; return in eax
add esp, byte 4 ; Clean up the stack
mov [newtime],eax ; Save new time value
sub eax,[oldtime] ; Calculate time difference value
mov [timediff],eax ; Save time difference value
push dword [timediff] ; Push difference in seconds onto the stack
push dword elapsed ; Push addr. of elapsed time message string
call printf ; Display elapsed time
add esp, byte 8 ; Clean up the stack
;;; Everything after this is boilerplate; use it for all ordinary apps!
pop edi ; Restore saved registers
pop esi
pop ebx
mov esp,ebp ; Destroy stack frame before returning
pop ebp
ret ; Return control to Linux
[SECTION .data] ; Section containing initialized data
timemsg db "Hey, what time is it? It's %s",10,0
yrmsg db "The year is 19%d.",10,0
elapsed db "A total of %d seconds has elapsed since program began running.",10,0
[SECTION .bss] ; Section containing uninitialized data
oldtime resd 1 ; Reserve 3 integers (doubles) for time values
newtime resd 1
timediff resd 1
timestr resb 40 ; Reserve 40 bytes for time string
tmcopy resd 9 ; Reserve 9 integer fields for time struct tm