Writing functions in assembly language with djgpp ================================================= revision 2 (9/8/98) (c) Copyright George Foot 1998 Warning: This information may not be correct. No warranty. I disclaim all liability for any damage it may cause. Some of this came from good documentation, some from looking at other people's source code, and some of it is probably just guesswork. If you're frightened, read the docs yourself. Throughout this document I'm talking about writing your own assembly language source files -- not about using assembly language inline in your C source code. Inline code has to obey different rules, doesn't need to do function-like entry/exit code, and can't really access parameters and local variables directly. It's assumed that you know the AT&T syntax for 80x86 assembly language; if not I suggest you read Brennan's guide to inline assembler in djgpp: http://brennan.home.ml.org/djgpp/djgpp_asm.html 0) Contents ----------- 1) Calling conventions a) Registers to preserve b) Return values c) Parameters d) Local variables e) Setting up frame pointer f) Example 2) Global symbols a) Accessing global symbols (e.g. externally visible variables in C) b) Making your own symbols c) Temporary local labels d) Example 3) Debugging a) Using symify's tracebacks b) Using a debugger to disassemble c) Adding debugging information to your code $) Credits a) Documentation b) Mailing list traffic c) Source code ?) Feedback 1) Calling conventions ---------------------- a) Registers to preserve ~~~~~~~~~~~~~~~~~~~~~~~~ You must preserve EBX, ESI, EDI, ESP, EBP, DS and ES. EBP is the calling function's frame pointer, if it had one; see section 2. b) Return values ~~~~~~~~~~~~~~~~ Integers of 1, 2 or 4 bytes are returned in EAX. You don't need to zero the unused parts when returning 1 or 2 byte integers. Integers of 8 bytes (gcc's "long long" extension) are returned in EDX and EAX, with EDX holding the high 4 bytes and EAX holding the low 4 bytes. Pointers are returned in EAX, as 4 byte integer offsets into your data segment. Floating point values are returned on the top of the FPU's stack. Structs are returned via a pointer. The caller pushes a pointer to a block of memory in which to store the returned struct, after pushing the parameters (see below). The called function then copies its result into the pointed-to memory block before returning. I think you're meant to leave the pointer in EAX on exit, too. Note that if your function returns a struct, all the information below about parameters will be slightly out. The pointer to the struct return memory block is effectively the first parameter, with all your other parameters moved up the stack one place. c) Parameters ~~~~~~~~~~~~~ Parameters are pushed onto the stack from right-to-left before your function is called. Parameters whose sizes are not multiples of 4 bytes are padded with garbage. Long longs are pushed as two 4-byte blocks, little endian (i.e. EDX is pushed first). Double precision floating point numbers also occupy 8 bytes. Structs are written to the stack in the same way, having been padded. If you're not returning a struct (see above), the first parameter is at ESP+4 on entry. If you're returning a struct then the pointer to the return memory block is at ESP+4 and the parameters start at ESP+8. So, if your parameters only include simple parameters, no more than 4 bytes each in size, and you're not returning a struct, your stack will look like this on entry: ... ... ESP + 12 parameter 3 ESP + 8 parameter 2 ESP + 4 parameter 1 ESP return address (goes into EIP on `ret') For the remainder of the tutorial we'll assume that the above is true, i.e. that your parameters are simple and you're not returning a struct. d) Local variables ~~~~~~~~~~~~~~~~~~ If you want local variables, the simplest thing to do is to reduce ESP to reserve some memory on the stack. How you use this memory is up to you; try to keep the variables aligned appropriately (i.e. make their offsets from ESP multiples of their sizes), though, and make sure the block you reserve is a multiple of 4 bytes long, to keep the rest of the stack aligned. Don't try to access local variables in this way using inline assembler, though -- you shouldn't guess where exactly the compiler is putting the variables. Use extended inline assembler instead to get your parameters and local variables. e) Setting up a frame pointer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In (c) and (d) above, the locations of parameters and local variables are determined relative to ESP. This isn't desirable, becuse ESP will change if you push or pop things during your function. The common way to solve this problem is to set up a frame pointer, which will point to our call frame. We save the initial value of ESP into EBP, which is the frame pointer. Then we reduce ESP to allocate local variables. Our parameters now have positive offsets from EBP, and our locals have negative offsets. We can fiddle around with ESP as much as we like, but we must keep EBP constant during our function or, again, we'll lose our variables. EBP must also be restored when we exit our function. This is because the calling function will probably be using it as a frame pointer, and will require it to be untouched by our function. The simplest way to preserve it is to put its old value onto the stack just before copying ESP's value into it, and to pop the old EBP value just after restoring ESP at the end of the function. We end up with this entry code: pushl %ebp /* save frame pointer */ movl %esp, %ebp /* set up our own frame pointer */ subl $X, %esp /* reserve X bytes for local variables */ The stack after this code looks like this: ... ... EBP + 16 parameter 3 EBP + 12 parameter 2 EBP + 8 parameter 1 EBP + 4 return address (goes into EIP on `ret') EBP old frame pointer EBP - 1 top of local variable space ... ... EBP - X bottom of local variable space with ESP equal to EBP - X. Bytes from EBP - X to EBP - 1 inclusive are reserved for your local variables. To undo this setup, we just need to unallocate the local variables and pop the old frame pointer back -- then we can `ret'. EBP and ESP will have been correctly preserved by this manoeuvre. movl %ebp, %esp /* restore ESP to value before allocating locals */ popl %ebp /* restore old frame pointer */ ret /* return */ The advantages of using a frame pointer, then, are that we can find our parameters and local variables easily even if we use the stack for other things, and that we'll get a proper call frame traceback if our function misbehaves. The disadvantage is that it uses a register which could be used for other things. If you're not using the stack (much), don't mind forfeiting the call frame tracebacks, and are prepared to expend a bit more effort working out where parameters and local variables are, you can skip all this and use the EBP register just like any other (but you must still preserve it, of course). f) Example ~~~~~~~~~~ Here's a function body that sets up the frame pointer, allocates space for one 4-byte integer, plays with it and the first parameter on the stack, then exits returning the value 1. pushl %ebp /* store old frame pointer */ movl %esp, %ebp /* set up our frame pointer */ subl $4, %esp /* reserve 4 bytes for local variable */ movl 8(%ebp), %eax /* load first parameter into EAX */ addl $3, %eax /* add 3 */ movl %eax, -4(%ebp) /* store in the local variable */ movl $1, %eax /* return code of 1 */ movl %ebp, %esp /* unallocate local variable */ popl %ebp /* restore old frame pointer */ ret /* return */ 2) Global symbols ----------------- a) Accessing global symbols (e.g. externally visible variables in C) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To access a symbol, you just write its name. This is equivalent to the address of the symbol. C variables and functions have an underline prepended to their name to form the symbol name, e.g. `main' becomes `_main'. C++ functions are mangled; if you're dedicated you can look this up somewhere, but the simplest thing to do is probably to ask for them to remain unmangled by using `extern "C"' in their declarations. b) Making your own symbols ~~~~~~~~~~~~~~~~~~~~~~~~~~ Any valid C variable name is an allowable symbol name; in fact some additional characters are valid in symbol names, but that's not too important. If a symbol name starts with `L' (capital) then it cannot be visible to other modules (and hence can't obscure another symbol's name). This has little to do with being able to access symbols from other modules; see below. If you want it to be referred to from C code then you must prepend an underscore, as mentioned above. If it's a function and you want it to be callable from C++ you must either mangle it or declare it `extern "C"' in your header files. To cause a symbol to be exported, use ".globl symbol_name". Also, put functions in the `.text' section. See the example below. So a symbol will be visible to other modules only if it has been marked with ".globl" and does not start with the letter `L'. In addition, C and C++ compilers automatically prepend a `_' to variable/function names. c) Temporary local labels ~~~~~~~~~~~~~~~~~~~~~~~~~ You can use numbers to specify temporary local labels, which aren't designed to be used from very far away. According to the documentation you can only use numbers 1 to 9, but the djgpp library sources use two digit numbers in places. When you refer to these, you write `1f' or `3b' to mean "the next defined label 1" or "the previously defined label 3". See the example below. d) Example ~~~~~~~~~~ Here's a `main' function that demonstrates some of the above concepts. .text .globl _main _main: pushl %ebp movl %esp, %ebp pushl %ebx /* we must preserve EBX */ movl 8(%ebp), %eax /* that's argc */ movl $1, %ebx 1: testl %eax, %eax ; je 2f /* if EAX is zero, jump forwards to label 2 */ shll $1, %ebx /* double EBX */ decl %eax /* decrease EAX */ jmp 1b /* jump backwards to label 1 */ 2: xorl %eax, %eax /* zero EAX */ 1: testl %ebx, %ebx ; je 1f /* if EBX zero, jump forwards to next label 1 */ addl %ebx, %eax /* add EBX to EAX */ decl %ebx /* decrease EBX */ jmp 1b /* jump back to most recent label 1 */ 1: popl %ebx movl %ebp, %esp popl %ebp ret Note that in the above example, all of the labels could have been `1'! The jumps go forwards or backwards to the first label of that number that they find. Also note that it doesn't matter if you have duplicate numerical local labels. 3) Debugging ------------ a) Using the default crash dump ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The normal djgpp crash dump will still appear, of course, if your assembler code misbehaves. However, since you now know exactly what the code is doing at the assembly language level, the register dump is now much more useful than it is when debugging C code. The call frame traceback EIPs will still make sense if you set up a frame pointer as mentioned above in section 1(e) above. You won't get good information back from symify though; it will fill in the function name as usual (unless you strip the symbol information completely) but it won't tell you the source file and line number. The compiler provides this information by adding debugging tags to its assembler output; when it's just assembling from your hand-written code it does not produce this information though. Section (c) below shows some ways to add this information yourself. b) Using a debugger to disassemble ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can use a debugger such as fsdb to find out exactly where the crash occured. Personally I find this a bit cumbersome though. If possible, you can run your program inside the debugger; the debugger will then catch it as it crashes and should show you what point was reached. Alternatively you can use the normal crash dump. You can look up the EIPs in the traceback, or the EIP in the register dump, using the debugger to find the instruction the computer was executing when it crashed. You'll need to figure out how to work the debugger first, though. c) Adding debugging information to your code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is what I personally do. It's pretty simple. All we want to do is add the filename and line number information, so that symify can tell us more accurately where the problem lies. Naming the file is simple; just use the `.file' tag: .file "myfile.s" Line number information is stored relative to the start of each function. You need three tags to mark a function -- first a `.def' for the function's name like so: .def _main; .val _main; .scl 2; .type 044; .endef Just copy that out, changing `_main' to your own function name in both appearances. That line goes before the start of the function. The ".type" gives information about the type of the symbol. Debuggers might get confused if you lie about this. 044 is octal for 0x24. That's a function returning `int' (as `main' should). In general, 0x2t is a function returning type `t', where `t' is taken from the following table (only selected types are shown): void t = 1 char 2 (unsigned: c) short 3 (unsigned: d) int 4 (unsigned: e) long 5 (unsigned: f) float 6 double 7 Just after the start of the function you need to tell the assembler that the function has started, by defining a `.bf' object (Begin Function). Put this sort of line just after the function label: .def .bf; .val .; .scl 101; .line XXX; .endef Again, you can just copy this line. XXX should be replaced with a number. Normally this is the line number of the start of the function within the source file, but in practice it's simpler to use a slightly different system, I find. More info below. Within the function, you put `.ln' tags before each instruction whose line you want to mark. It doesn't matter if you miss out lines (the compiler does this a lot when optimising anyway), and it doesn't really matter what numbers you write. It's probably best to make them strictly increasing, of course. The initial line number is `1', just after the `.def .bf' line above. You also need an `End of Function' marker; this goes just before the function's exit code (i.e. before popping saved registers, restoring EBP, etc). The line you need is this: .def .ef; .val .; .scl 101; .line XXX; .endef Here, XXX is the relative number of the last line within the function. Normally this `.def' line comes immediately after a `.ln' tag; in this case you set XXX to the same number as given in the last `.ln' tag. That's all that is required to get symify to tell you your filenames and line numbers. However, it's pretty awkward to get the line numbers right -- especially if you start inserting lines; it throws everything out of synch. I suggest that you don't make any effort to make the line numbers correspond to the true positions within the .s file. The assembler won't care, because it's used to this; when gcc compiles C code, the line numbers it generates correspond to the .c file, not the .s file. My system is to mark all functions as beginning on line 1 of the file. Then you place your `.ln' markers at appropriate points within the functions. Make the numbers increase; not necessarily in steps of 1. In the days of BASIC and line numbers, people used to use steps of 10 or 100 so that they could insert more lines later; the same idea applies here, although I'm not sure if any debuggers really care what order the line numbers come in. If you use my system, symify will tell you the function, filename and fake line number. You can then open that file, search to the beginning of that function, and search on from there for ".ln " followed by the fake line number it gives. This takes you to the start of the block of instructions that caused the problem. The advantage of this sort of system is that you can insert or remove lines as much as you like; it doesn't matter, because the fake line numbers are not the same as the real line numbers. $) Credits / Bibliography ------------------------- a) Documentation ~~~~~~~~~~~~~~~~ The official documentation is that of `as', the GNU assembler -- type "info as" to read it. Brennan Underwood's guide to inline assembler is of course required reading for anyone wanting to learn AT&T syntax in a hurry, or just anyone who doesn't want to dig through the `as' docs in as much detail as Brennan did. URL: http://brennan.home.ml.org/djgpp/djgpp_asm.html b) Mailing list traffic ~~~~~~~~~~~~~~~~~~~~~~~ Nate Eldredge posted a useful summary of calling conventions to the djgpp mailing list a while ago -- search the archives for that: Subject: Re: REQ: Wait for Retrace function Date: Sun, 4 Jan 1998 14:29:37 -0800 (PST) c) Source code ~~~~~~~~~~~~~~ DJGPP library sources, e.g. djlsr201.zip -- src/libc/crt0/crt0.s Allegro library sources, e.g. alleg30.zip -- allegro/src/xgfx.s Learning by example isn't always the best way of course. ?) Feedback ----------- Please let me know what you think of this document. It started off as a quick-reference guide to me when programming, and I developped it on from there into more of a brief tutorial. If you have comments, please send them to me: george.foot@merton.oxford.ac.uk If there are more topics you'd like this guide to cover, please do let me know and perhaps I'll add them.