So I started dabbling with assembly language programming a couple of days ago. This was the next logical step in the “going lower down” move I have been doing ever since I started writing programs in Visual Basic some years ago (there, I admitted it). Since then I went through C#, Java, C++, C, and now finally assembly. And it is fun to watch a program die in so many innovative ways. It is helping me understand the internals of a program much better.
One of the first things I learned about assembly programming was that I needed to use completely different syscall numbers and instructions for x86_64 as compared to i386. For example, the syscall number for exit on i386 is 1 while on x86_64 it is 60. Same goes for write — 4 on i386 and 1 on x86_64. I spent half hour trying to figure out why my program was calling fstat on x86_64 while a similar program built with –32 would work fine.
Crossing all these hurdles, I finally wrote a slightly more complicated (but still useless) program than a hello world. This is a program that takes in an integer string through the command line, converts it to an integer, converts it back to string and prints it back out. Pretty useful huh :)
Now for the interesting part in the code. I always thought of dynamic memory allocation as something you can only go through the OS using the brk() and/or mmap() syscalls. Generally, we do this indirectly through malloc() and friends. But what I ended up doing in my program is allocating memory on the stack on the fly. Here’s the code snippet:
1 2 3 4 5 6 7 8 9 10 11 | movb $0x0a, (%rsp) decq %rsp next_digit: movq $0, %rdx divq %rdi addq $0x30, %rdx # Hack since we cannot 'push' a byte movb %dl, (%rsp) decq %rsp |
The complete code along with the makefile is at the end of this post. You can build it if you have an x86_64 installation. What I do above is simply:
- Read a digit from the number
- Move the stack pointer ahead to make room for a byte
- Store the ASCII representation of that number into that byte
I could not use the push instruction itself since it can only push 16, 32 or 64-bit stuff on to the stack (with pushw, pushl, pushq). If you push a single byte value, it will be stored in one of the above sizes, not in just 1 byte. What I wanted was to create a string on the fly without limiting myself to a fixed size array, so this seemed to be the only approach. While this works, I still need to find out a few more things about this:
- Is it safe?
- If it is safe, then is there a similar way to do this in C without embedding assembly code? This would be really cool, especially in usage scenarios such as the above. Admitted that the above scenario is pretty useless in itself, but I’m sure there must be similar examples out there that are at least a little more useful.
The code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | .section .data usage: .ascii "Usage: printnum-64 <the number>\n" usagelen = . - usage .section .text .globl _start # Convert a string representation of an integer into an int .type _get_num, @function _get_num: push %rbp movq %rsp, %rbp movq 0x10(%rbp), %rdx mov $0x0, %rcx mov $0x0, %rax nextchar: # Iterate through the string movb (%rdx), %cl cmp $0x0, %rcx je call_done subq $0x30, %rcx imulq $0xa, %rax addq %rcx, %rax incq %rdx jmp nextchar # Convert a number into a printable string .type _print_num, @function _print_num: push %rbp movq %rsp, %rbp movq 0x10(%rbp), %rax movq $0x0a, %rdi # Hack since we cannot 'push' a byte and increment # %rsp by only 1. push will push whatever it has as # a 16, 32 or 64 bit value (pushw, pushl, pushq) movb $0x0a, (%rsp) decq %rsp next_digit: movq $0, %rdx divq %rdi addq $0x30, %rdx # Hack since we cannot 'push' a byte movb %dl, (%rsp) decq %rsp cmp $0x0, %rax jne next_digit movq %rsp, %rbx addq $0x1, %rbx movq %rbp, %rcx subq %rsp, %rcx push %rcx push %rbx push $0x01 call _write jmp call_done # Wrap around the write system call .type _write, @function _write: push %rbp movq %rsp, %rbp movq 0x10(%rbp), %rdi movq 0x18(%rbp), %rsi movq 0x20(%rbp), %rdx movq $0x01, %rax syscall jmp call_done # I always do this when I am done with a function call call_done: movq %rbp, %rsp pop %rbp ret #Program Entry point _start: # Command line arguments: # The parameter list is: # argc: The number of arguments # argv: The addresses of all arguments one after the other # They can be popped out one by one pop %rax cmp $0x2, %rax jne error # Pop out the first arg since it is the program name, but # keep the second so that it can be fed into the next function pop %rax call _get_num push %rax call _print_num jmp exit error: push $usagelen push $usage push $0x2 call _write movq $0xff, %rax exit: movq %rax, %rdi movq $60, %rax syscall |
The makefile:
1 2 3 4 5 6 7 8 | 32: as --32 $(target).s -o $(target).o ld -melf_i386 $(target).o -o $(target) 64: as $(target).s -o $(target).o ld $(target).o -o $(target) |
If you save the source as foo.s, you can build it with:
1 | make target=foo 64 |