 Hey guys, welcome back. Skitso on episode five, topic today is printing integers. We're going to do base 16, base 10, base 8, and base 2. And that's a big step for us because it enables us to actually print out math results. So we can actually do scientific computing and print out the answers. Up until now, we've not had the ability to do that for the most part. It'll also give us the ability to make debugging tools, which will help us to solve problems in our code going forwards. We can now register contents, print out the stack, print memory, as well as print 2D arrays, all in this video. So if that interests you, check out what I'm going to show you. So just to review, how do we print strings to the screen in a previous video? Well, we basically, we had a character array like this. And we knew the start address, which was the address string to print, but it's also the address of the first letter, the first byte, which is A. These are ASCII bytes, I should specify. And then we also knew the length of the string as well. Then we had a function called print cares, which would take the address of the first byte and the number of bytes. And it would basically copy them into a print buffer, which say is 4k long or whatever you pick. And basically you'll then have a bunch of bytes in a print buffer. And then if the print buffer ever fills up to the end, it will flush automatically. If not, you can manually call print buffer flush, which will flush those contents of the buffer to the screen or to a file. So it's pretty straightforward, I think. And so the question is, what's the analog for this for numbers? So let's see you had a number. Let's say it's the date that George Bush demolished the Twin Towers in register RSI. So it's just accountable value. We need a function. In this case, I'm calling it print int D. D means decimal. We'll have a print int H for hex, print int B for binary, print int O for octal. And that function will do two things. One, it will basically create a character array like this. So it will take the nine from the number, put it here, the one from the number, and put it here, et cetera. And it will also then take that character array and then put it in the print buffer. So same as before. So this is the analog of the print characters function. In effect, it actually probably will call the print characters function, but its real job is just to convert this integer quantity into a character array. That's the job of that function ultimately. And then again, you can flush that buffer to the screen. And so ultimately, it boils down to printing integers is just converting integers to a character array and then printing them in the usual fashion of printing character arrays. And so we need some memory space to hold that character array. This array of Ascii bytes has to be somewhere in memory. We can't just make it up. It can't be in a register. What if it's a very, very long number? And so the question is, where should we put that array? And so I went out and I asked the cast of Courage the Cowardly Dog what they thought and hear what they came up with. So Muriel said, we should make a new number buffer memory location like the print buffer. Basically copy this, make another buffer called number buffer, and just put the values directly in there. That's fine, I guess. You can do that. Nothing wrong with that. Just a little bit extra memory. You don't really need that, but that would work. Again, I'm not going to judge these ideas. If you want to actually implement this in these ways, you can do it. They would all work. Another option that you just came up with was why don't we dump the characters directly in the print buffer? So he's saying, instead of putting this in some intermediate location and then copying that to the print buffer, why don't we just put it there first? Instead of just the numbers entirely, just put the numbers here in the first place. That would work. The only problem with that, it blows down to the algorithm. So when you want to parse digits out of this, realistically what you do is you do the lowest digits first. So you start with the one, then you do the zero, then the other zero. And the problem with that is that those are not the first bytes printed. Remember, when you copy these ASCII bytes onto the print buffer, the first byte has to be the nine byte. And so you kind of have to know, if you're going to use an algorithm that goes from right to left, you have to know where to put these values in. You have to know beforehand how long the number is. And then you have to offset each one, which you can do. Again, that would be a perfectly functional solution to the problem. But it just requires a little bit more math that I'm willing to put into this very simple algorithm. I'm going to do what courage recommends, which was, why don't we push those numbers, push those bytes, I should say, onto the stack? And the beauty of that idea is that the algorithm itself goes from right to left in the way that we're going to get these ASCII values out of the number. And also, the stack grows backwards as well. So two wrongs make a right. Two negative signs is positive. We're going to basically leverage the fact that the stack grows downwards and basically reorder our character array the right way. So it's a pretty clever idea, good idea of courage. So to do that, I'm going to mention just the quick details about the stack frame. And if you look up algorithms for printing integers or any kind of number to the screen or to a file online, they will always talk about the red zone. And first off, that's a typo. They mean to write chalk zone. What the red zone is, basically on Linux and on FreeBSD and stuff like that, Rudy, here with his magic chalk, he basically draws a grid of 128 bytes. And basically, you can use that grid for memory stuff that you're working on in the meantime. If you don't have enough registers or whatever you're working on, you can use that. But the caveat is that this red zone is always 128 bytes of space under the stack pointer. So wherever the stack pointer is, this is just space you can use. This is space that the OS guarantees you can use behind the scenes. So if you access beyond this, you might get a problem, but if you access it within 128 bytes of the stack pointer, you should be a-okay to do so. Now, why is this useful? Well, this is only useful for what's called leaf functions. Basically, they're functions that don't call anything else. You know, they're at the end of the branch on the tree. They don't call anything else. And so basically, it's just a space where you can put values in the meantime. And this will work just fine. You can use the red zone, which we're kind of gonna do the same thing. But it's basically, if you wanna do buffer printing that we're doing here where we're filling up a buffer, you always have another function that you wanna call. And so what we're gonna do here, this is not going to be a leaf function. This is a function that calls other functions. And so it won't be called the red zone for us. It will be called just the stack for us. So we're gonna be just, like Kurt said, we're just gonna be pushing our ASCII values onto the stack one by one. And we're gonna be growing the stack down one by one. And then once we're done with the number, we're going to be calling another function, probably print characters, that then prints the stack contents to the screen. So not quite the red zone, but the similar idea. So here is the algorithm. I'm showing you the algorithm for the hexadecimal version of print int. Just because I think it's representative and it's probably one of the hardest ones. And so let's say you have the input number to print in RSI, register RSI. And here's that value in ASCII as far as I'm aware. So here's the algorithm how it works. The first thing you do is you grab the lowest nibble of your number. So basically you're just gonna end it with 15 or 0xf or 0b1111 or whatever. That will basically take just this character out of your number. Then, so in our case that would be just the digit one. Then you're gonna convert that nibble, that value between zero and 15 into an ASCII character. And so in our case that was a one, that was this. So the hex character for one is just the digit one and the ASCII vicar that is 49. And so in that case, basically it's taking the nibble value and adding 48 to it. And that holds true for zero. It holds true for one, two, three, four, five, six, seven, eight and nine. Those are all 48 more than the input nibble. So basically we're gonna be just printing out a byte that is 48 more than whatever the nibble is for numbers zero through nine. And that works fine for decimal because decimal only has numbers zero through nine. It works fine for octal. That only has numbers zero through seven. It works fine for binary, which has numbers zero and one. The problem with hex is you have these letters as well. You have A, B, C, D, E, and F. And I picked the lowercase letters. You could pick the uppercase letters if you wanted. In that case, you have an extra offset into the conversion. So if the number is, if the nibble is greater than nine, you have to add an extra, what is it, 39 to your ASCII value. But yeah, basically you just have a way to convert between the input nibble and your byte value to print. So once you've figured out that conversion in yellow here, then you push that byte onto the stack. So you've taken this one value out of the big number, you then convert it into an ASCII byte, then you push it onto the stack. Then what you do is you take your input number and you shift it right by four bits. What that does is it basically pops off the one. Then you check if that number is zero. It's not yet zero. And then you loop until the number is zero. So you repeat. In this case, your lowest nibble is C. You're gonna end this number with 15. You're gonna get C. You can look at the table, C is 99. You're gonna push 99 onto the stack. You're gonna shift it right again by four bits, take nine out of the number, ASCII 557 onto the stack, et cetera, et cetera. You keep looping until this number is zero. Once you're out of things to print, the number will be zero and you can stop your algorithm. At the end, though, remember, you still have to print out this prefix. For binary, we have zero B. You can see here. For octal, we have zero O. Prefix indicates octal. Hex, we have zero X. And in decimal, you don't have one, but. So we're gonna push the value for X and the value for zero onto the stack in that order. And then lastly, we're going to call that print characters function on the stack character array now that we have memory defined for our number. And now that we know how long that character is, et cetera. And this will just work. And this algorithm is valid for hex, but with just a very slight tweak, you can make it work for octal. What if you just change this to a three and then you change this to a seven? It should pretty much work for octal. And then binary would just be taken off those two and making this to be a shift number by one bit each time. That would be binary. And then decimal, a little bit harder because decimal, you have to divide by 10 each time and take the remainder of the division. But again, very simple. The code's available. You can check out how that works. In fact, I'll probably pull it up right now. So we're going to have four examples here. We have example A, which is printing in. That's going to show us how to print those base 16, 10, 8 and two numbers. I'll go through that in a second. Then I have example B, which is printing registers. So this will print out the entire 16 general purpose registers to the screen. And for that, we're going to use function pointers, which is actually pretty easy in assembly. It's not so easy in seed, but in assembly, it's really straight forward. Example C is going to be printing arrays. So 2D arrays to the screen with a nice MATLAB or octave formatted output. This way, if you're debugging your code, you can just, or even if you're printing stuff as a solution, you can just copy that into MATLAB and then use it for something else down the road if you want to, which I've done so in the past many times. And then lastly is example D, which is dumping memory. So we're going to talk about dumping the stack as well as dumping just memory contents to the screen. And that's inspired by the hex dump command. If you guys are aware about how that works, you can check it out. Run it on a binary and you can see what it prints. It prints out basically the byte values in memory. So let's pull up this code here and take a look. So I'm going to first show you those functions that print out integers to the screen because I think they're interesting. Print int, I'm so dumb. Okay, so this one is, let's do the hexadecimal one first. We just talked about that. So here's how the algorithm plays out. You can see first we're including that print characters because we're going to call print characters to print our characters. And this function just takes two inputs. It takes the file descriptor on the off chance that this character array fills the buffer while we want to know where to print the buffer to. And it also takes the value to print. So let's say value seven would be in RSI. So first things we save our registers. We save all registers. This con convention preserves all registers. So we save everything that we touch and then we save our base stack pointer. So you want to be able to know what our stack is in the first place before we start pushing garbage onto the stack. We want to know where our stack was because we don't know how big the number is. This number could be seven. This number could be seven trillion. And it's going to be a different amount of bytes pushed onto the stack based off how big that number is. So we want to save the initial stack pointer so we don't get lost. We can restore that at the end. Okay. And so the algorithm you can see here is we're going to keep the value in RSI and the low bits we're going to shift off four by four from that value. And so here's the loop that we're going to be implementing. Very simple. We're taking the low byte of RSI. So yeah, I'm putting it into AL. So the low eight bits. And then we're going to basically chop off the and just keep the low four bits. So this is that and with 15 I was talking about we're going to end with zero B 1111. This keeps us the low nibble of the number. Then we're going to add 48 to it. In this case this converts you know, nibbles zero through nine to ASCII values zero through nine as you can see. And then we have a little comparison here that hey, is the number more than that? Should it have been an A or a B or a C or a D or an E or an F? If so adjust that. So it's just a basic comparison. If we're below that number we skip otherwise we're going to increase our byte value a little bit just to adjust the ASCII values to be zero through nine A through F. Then you can see here, you probably can just push a byte to the stack here. I'm documenting the pointer manually. Decorating RSP that moves the stack pointer down by one byte and then I put the ASCII value that we just created into that location on the stack. And I think you can just push a byte to the stack that would also work. And then you can see here we're shifting RSI by four that basically moves that nibble off. The lowest nibble becomes garbage. And now we have a new lowest nibble at the, you know, least significant part of our number. And then we test if that number is more than zero. So this basically tests RSI against itself. If it's non-zero, we go back to the top and we keep repeating this process of parsing one nibble at a time and putting onto the stack. Once we fall out of that loop, you know, that is we're out of numbers to print and RSI is zero. We push our zero X onto the stack. And then lastly, we do some mathematics here to evaluate the length of that number. So here you can see we're computing the length of that character array in RDX. You know how many bytes that number was. We're putting that stack location into RSI and then we're calling print characters on that stack location. We should just say stack. And yeah, that will print the number, that will put the number into the print buffer and or flush it out, depending on whether or not we fill out the print buffer. And lastly, we pop our save registers off the stack and return. So yeah, very simple algorithm, good idea of courage. And it's very simple to convert that to octal. Again, the only difference is we're taking an and with seven, not 15. We only care about the lowest three bits, not the lowest four. And we shift it right by three every time, not by four. That's a very similar process. And the beauty of octal and decimal and binaries that they don't require that extra logic of alphabet characters like A, B, C, D, E and F. You don't have to have that comparison in here. Besides that, it's pretty much the same thing. Binary again is the same thing. We're only ending it with one this time. So just the lowest bit is saved. And then we shift out just one bit at a time. And then lastly is decimal. The complexity here is that you can't just shift because it's not a multiple, it's not a power of two. Whereas base two, base eight, base 16, we're powers of two. This one, you can't do that. You have to basically have a divisor. And so here we actually have a divisor in RDI and we're gonna keep dividing the number RSI or whatever it is by 10 every time. And that remainder is our byte to print. That's the one difference. But the other difference is that you have to have handling of negative numbers. Cause whereas octal and des... Or sorry, octal, hexadecimal and binary, they have like a two is complement way to show negative numbers or whatever. In decimal, we don't have that. We just have a negative sign. And so we have to basically handle negative signs manually. If you're curious about how that works, basically we're just saving the original value. And then at the very end, oh, we're saving the value. We're printing out the positive number. And at the very end, you can see here, we've, if we needed to, we check the original value. Is it negative? If so, put a negative sign as well. So a very simple algorithm. With that out of the way, I'm going to show you these examples here. The first one was example A. Let's look at the code first. So all this does, let me show you. So basically, it just includes a couple of functions and includes print characters, print all the int options. So binary, octal, hexadecimal, as well as exit. And then at the bottom here, you can see I've got a value defined, one, three, three, seven. And a grammar of just the new line character. And so the idea is, you can see here, we're going to move our terminal file descriptor out here, standard out into RDI. And then we're going to call this different print routines on different things. So we're going to print out the number in binary, followed by a new line. Print it in octal, followed by a new line. Print it in hex, followed by a new line. And then print it in decimal, followed by a new line. And then we're going to just leave. We're going to flush the buffer and then leave. So if I run that, you'll see that we have, in this case, I presume these are the binary, octal, hex and decimal versions of that number. And they work just fine. I will change one thing though, just to show you how this works. What if we change this number to a negative? And we rerun it. You can see now, we have this long two's complement or whatever it's called, a string of ones to indicate a negative number. Cause this is a quad word. This is a very long number in binary. In octal, it's the same thing. We have a bunch of ones, same in hex, a bunch of ones. But in decimal, the beauty of decimal is that you just have a negative sign. You don't have to worry about all those leading ones to indicate a negative number. So cool. That's that. Let me change that before I forget. Okay. Example B, that was printing registers to the screen. And so, let's go to the code and I'll show you what we've got here. So, I have a function called print registers. And what that does is it takes, well, actually, you can see here, I've used, I forgot about this. I actually used the stack to pass inputs because I didn't want to pass inputs and registers because that defeats the purpose, right? If this function prints registers to the screen, why am I putting input values and registers? That just seems like a waste to me, right? I'm losing my ability to check the value in RDI and RSI if I pass these inputs in RDI and RSI. So here, I use the stack to pass inputs, which again, I think that violates the system 5 ABI, but it's okay. We're not following that in the first place. What it does is, so here, I've put some test values in different registers. I've put 13 and REX, you know, all these numbers in those registers. And then I've, here you can see, I gotta have a function pointer. So, it's pretty cool. Basically, I'm pushing the address of the function print int H. So, that was the function that prints ints in hexadecimal and I'm putting that onto the stack. Then I'm pushing the file descriptor out onto the stack. And then we're calling print registers, which again, takes things off the stack, these inputs, and uses them and then dumps the registered contents. And you can see here that those would end up being at RSP plus eight and RSP plus 16. And let me actually show you how this works and then I'm gonna show you the actual code to make it work. So, if I just run this, you can see here I've dumped out all registered contents. So, I've got all those values in here in hex. Now, if I were to go back in there and change those to, change that to decimal, I can just change the function pointer that this function, print registers, is using to print. So, I can make that a D. In this case, we will see values 13, 66, 120, and negative one in those locations. So, if I run this, you'll see I've got 13, 120, 56, and negative one in those locations. So, I will now show you how that works. Print registers. So, you can see here, this function, it prints out registered values to a file descriptor that's put on the stack. It's not passed in a register and it uses the function pointer to print that's found also on the stack. And I won't go into details of how this function works. Basically, we are pushing all our values onto the stack, including the initial stack pointer location, which we have to save beforehand because whenever we push, we move the stack pointer. So, that's what that is. And then we have a loop down here, which basically prints out a string that indicates the register that we're talking about and here you can see I have a bunch of register names, Rx, Rbx, all the way to R15. And then we just basically pop off register values one by one. Kind of, I mean, not really popping them off, but just looping through them and calling the function that we've just pointed to. If you're curious how that works, you can step through this logic manually yourself on your own time the code is available online. So, let me just go back and change that to hex before I forget, because I will forget. Let's go to the third example, which was C. This one is very simple. This function is pretty similar to the previous one, but the fact that it doesn't print registers or prints out an array of ints. And so in this case, we're gonna pass a bunch of different things into this function called print array of ints. We're gonna pass the power descriptor, we're gonna pass the location of the first array address, we're gonna pass in addition to that number of rows and columns, as well as number of offset. Don't worry about what that is, I'll get into that in a later video, how we can use offsetting to help us make our code more generalizable. And then we have R9, that's gonna be our function pointer. Again, we can pick between binary, hex, decimal, octal for our prints. And so here you can see I've got an array. In memory, arrays are just, words, a quad words in a line. So in this case, I've drawn them out in a three by five matrix, but this is just as well a five by three matrix, or a 15 by one matrix. This kind of matrix is whatever shape you want. But here you can see I've defined it as a three row, five column array. I'm printing out as a decimal system. Simple process, if I run this, you can see I've got the array dumped to the screen. And I have it dumped in a way that if you wanted to copy this into MATLAB or into Octave or some other language like that, you can do so. So yeah, that works just fine. Last example I wanna show is example D. This covers two different things. It covers dumping of the stack. I say I have a print stack function as well as a print memory function. So the stack function basically will print out a number. So RSI indicates the number of bytes of the stack and above the stack pointer that I wanna print. And then this function pointer here, that's the function again that we're gonna use to print. We're gonna print it in base 10, and base eight, and base two. Print memory is very similar. Basically this just you have to pass in the first byte location. So let's say you wanna print out that array from before you pass in the address of the very first element as well as it takes the function pointer again if you want it to be base two, base eight, base 10, base 16 as well as the number of bytes to print. And so you can see here I've got that same array at the bottom and here you can see I'm, this is the first part is the stack. So I'm dumping out the stack content. So here you can see I wanna print out 16 numbers on the stack. So 16 entries of the stack up from where we are currently. And I'm also pushing the number onto the stack here, seven, seven, seven. Let's print it out in, nah, let's leave it in hex. And you can see that this block of code will print out the stack. 16 entries worth of stack to the screen. And the second part will print out the array. So this will print out this memory, this memory chunk just in bytes. Again, it will print it out in hex. You can see I had this line commented out because I've already defined print int h here. I don't have to redefine it down here just to waste of an instruction. So if I run this, let me clear the screen. If I run that, let me pipe it into less. So basically it made a binary for us. Let's run that binary, pipe it into less so we can look through it. So the first part of that, that was printing out the stack. So here you can see all that data on the stack. And a lot of this stuff is just made up. We didn't put this stuff there. It was just there when the program started. RSP plus 16, that might be argc, right? That might be the argc value, number of command line arguments that we passed in. And I think this number here is seven, seven, seven in decimal. That's the number we put onto the stack just now. And this is the current return address of that function. I do believe. And then down here, these are those bytes for the, that integer array that I showed you before. This is just one byte at a time. And then the left, you can see the address in memory. So this is the address of the first byte. And you can see we have one, two, three, four. We have eight bytes per line. So yeah. And again, you can change this to be decimal if you want it as well. Let's just do that really quick. Just to make it a little bit more sensible for us humans. Yeah. So if I run that into less, you can see here. Yeah. So this was the argc value. This was the value we just pushed onto the stack just now. And this is, I guess the return address for that function, which is fine. And then this is the memory for, that array I just showed you before, in decimal. It makes less sense in decimal, but yeah. So there was one more thing I wanted to show. What was it? Oh yeah, here's one thing. So if I run that binary and see a problem with 3BSD, right, look at this location of the one. This is supposed to be argc. That's the number of arguments we passed in. So when we run binary, there's only one argument. And that's the word binary. So if I run binary, you can see here that argc is in address RSP plus 16. But if I keep running this, sometimes you see here that it moved to RSP plus 24. And that's a problem with 3BSD. For some reason, their argc location is like non-deterministic, it's randomized. And that threw me for a loop for quite a long time. Anyway, just a weird oddity, that doesn't happen on Linux. You see that one hopping up and down like that. That's not a feature of Linux. Anyway, thanks for watching guys. Next videos we're gonna cover, I think we're gonna cover randomness. We're gonna cover timing things. And we're also gonna cover making a text-based UI. So stay tuned for those videos. Thanks.