Submind YouTube summaries
Thumbnail for calling conventions and debug logging | SCHIZONE EP-34

calling conventions and debug logging | SCHIZONE EP-34

Watch on YouTube

Video summary

In Episode 34 of the SCHIZONE series, the host addresses significant technical hurdles involving Linux compatibility issues and file corruption by proceeding without prepared slides to explore ARM assembly calling conventions and advanced debugging techniques. The episode focuses on implementing three distinct approaches to function calls: a traditional method that preserves all registers for safety despite its bloated nature, an efficient direct branch approach that destroys registers for small programs, and a hybrid macro-based solution utilizing X20 as a temporary register while conditionally saving clobbered data via stack operations. A central feature introduced is conditional debug logging controlled by a preprocessor flag; this allows the assembler to include verbose print routines similar to `strace` only when needed, enabling developers to log system calls and parameters without unnecessarily inflating the binary size during production builds. The practical application of these concepts culminates in the creation of a simplified assembly version of the utility "bin_extract," which replaces a complex shell script previously reliant on external tools like `xxd`. This self-contained program demonstrates how debug logging can be toggled via command-line arguments, showing that disabling verbose output and removing macro-generated stack operations drastically reduces binary size from approximately 4500 bytes to around 1700 bytes. The host analyzes various configurations of this extractor, noting that while fully enabled macros result in a larger footprint at roughly 488 bytes when stripped down further or embedded directly into logic without branches, the trade-off between code readability and binary size is carefully weighed against specific program needs. For very small programs where functions are invoked only once or twice, embedding code directly avoids the standard ARM overhead of eight bytes per call associated with branching and returning, offering slight performance improvements that may be negligible due to syscall limits but still relevant for minimalism. Despite these minor efficiency gains in stripped-down versions, the speaker maintains a preference for retaining some macro structure to ensure code readability, reserving fully embedded logic only for specific constraints. Looking ahead, this foundational work sets the stage for future projects involving hardware peripherals, where debug logging and macros will be essential for tracking device operations such as controlling LEDs without compromising the integrity of the custom assembler tools developed throughout the episode.
Read the full video transcript
Hey guys, welcome back. Skit zone series. Making some good progress today. Um, in more ways than one. So, Skit Zone episode 34. I had a heck of a time getting this ready today. Um, last night I was working on the slides. It was going okay. Got them done. This morning, woke up early, did some chores, came over, got in some last screenshots. I tried to at least spectacle my screenshot tool wasn't working. Okay, tried to fix that. Um, classic Linux problems. Couldn't get it to work. Tried a different one. That one didn't work. Tried a third one, Flameshot. That one worked. But when I installed it, I must have like broken my ffmpeg because OBS then wasn't recording when I went to record the video. Then I tried to fix that. Unfortunately, that broke my exorg or Whan session thing. Um, long story short, I bricked my whole computer basically. Classic Matthew problems. um reinstalled. Before I reinstalled, well, first off, it's 2025, so you have like three dros to choose from nowadays because they're all dead. Um you have uh Linux Min. That one barely works. You have KDE Neon. That's actually pretty good. And then you have Endeavor OS. That's what we're on right now. Um I don't know. Here we are. Um I saved two files before I cleared my PC. That actually it's on this USB. It was an SSH key and it was the slides and I double checked that it was on here. I plugged it in just now. It's not on here. It's corrupted. Can't read this. Just my luck. Then I went to try my Raspberry Pi to actually run the code to make sure everything still works. I couldn't SSH in. There was problems. The router had an issue. I'm on the network. I don't know what's going on. Then I recorded this video once. Halfway through I had a had an issue I had to fix. Anyway, I'm pissed. So, here we are. We'll do it in one take. Of course, there's no there's no slides. Just one slide today because it's all gone. Uh, you know how it goes. Usually, when bad things happen like this, when there's like 10 different subquests and they're all impossible, usually it's because God, in my experience, doesn't want you to do this. But every day, we disobey God. And so, here we are once again. Topic today. Well, speaking of a fresh start for like this OS and everything, right? I like fresh starts. They're kind of nice. They give you like new set of constraints, new set of requirements, new information. You can do things a different way. Um, it's fun. It's like Minecraft, you know. Hey, I'm playing Minecraft on a new world, but no chests. All the blocks have to be solid blocks on the ground or they can be stored in in like a furnace or something. You know, that's fun. You know, this same game, same parameters, but different constraints. And here we are in ARM assembly. A lot of the same constraints, a lot of different constraints, a lot of new tools. We have to use the GNU assembler, which is good in some ways and bad in many ways. Um, and so we can try some new stuff. I want to try some new calling conventions. I'll go through those today. Using macros. That's one of the cool things about the new assembler is that there's a little bit more powerful macro engine than in NASM. We didn't actually use that many macros in NASA. We did a couple here and there for debugging, but not much. So, I want to use more of those. I'll show you three options for calling convention that we can use. They go from efficient to easy to use in like a spectrum. Um, and there's some cool ways we can do that. Then we have um debug logging. One of the things I learned is that I was using srace like so often and like exit codes and stuff trying to debug things. The problem with assembly is when you're like bootstrapping stuff from the first couple of functions, you don't have any print statements. You have to debug with exit codes and then return those values to the shell blah blah blah. Anyway, it's nice to have verbose debug logging type things. And we're going to embed that stuff conditionally at compile time. It's pretty cool how that works. I mean, it's not it's not advanced, but it's pretty cool how it how it works. So, we'll show you that. And then we're going to implement two simplified versions of two of the core utilities. Well, really one utility which is like cat just to print fins. It's very simple. Um and then bin extract. It's not really a utility, but if you recall from the previous video, this boomer assembler thing, it has to ex um it basically creates an object file and most of that is trash. Um only the middle part is actually of any value to us. And so we can write our own assembly code to extract that middle part, make it executable, delete the old file, kind of like we were doing in the bash script. Those five or six lines we can replace with a single assembly file that we wrote oursel and it's much smaller and we know how it works. And that's the whole point of this whole project is to know how things work and to learn. So that's the plan today. No slides. We'll get right into the code. So let's see here. First things first, the column convention. To refresh your memory, what was the old column convention? Nexus 6. If we go back to let's see um uh lib, let's open up some random one. Math matrix. And then let's open up some random. Let's open up matrix uh transpose. If you recall how we did this, we basically whenever we called the function, we would push and pop all registers before and after the function. So if the function used R10, we use R9 here, R10, we're going to save it beforehand and we're going to put it back afterwards using the stack. That was the whole point. This way all the registers were preserved through the function call. They were all, I guess, call saved apart from the return value. So if you had a return value, there's none here. versus a void. But if there was rax as a return value, that wouldn't have been preserved. That would come through. Which is very nice. That means if I have a calling function with, you know, a thousand lines and it just keeps calling other functions and never changes around, then RDI and RSI and RCX, they're all going to be the same values all the way through unless any of the functions themselves as an output change them. So, it's very it was very nice, but it's kind of bloated. When is it bloated? It's loaded when you have a very small program because remember this gets included only once, right? This is an include file. This transposes matrices, right? So it's going to this is some some binary code 0 0 1 0 0 in memory just once. And so all these pushes and pops only take place once. Of course, you could say it's slow to have to push and pop all these things over and over again. Yeah, maybe. But who cares about that? It's not really a big thing. Really the issue is for small programs, what if you what if your whole your whole program was just one matrix transpose? You pass in a matrix, transpose it, print it out. Then you don't have to push and pop anything because what's the point, you know, like there was no value to doing that. Why do we preserve RBX? Who cares? It's it could have changed. So you wasted that instruction both in terms of runtime and also space in the binary. So that was one caveat, a very small minor thing. Um, so instead, what can we do this time? Well, I'll show you. The problem is when I show you this, you're going to see a bunch of stuff in there that's going to confuse you. Um, let's h let's just show you that. Ignore everything about the verbose logging when you see this file, okay? Because it's going to confuse you. Um, uh, let's see. Lib CIS. Let's open up. Oh. Let's read. So this is the read function. It's very simple. Read is just a system call. Remember all read is if you open up like the man page, it takes three parameters. It takes in let me show you. You can write right here. Sorry. Ignore all the stuff about the debugging read. It takes in three parameters. I didn't label them here, but it's obvious. The first one's a file descriptor. So what are you reading from? You open up a file. It's an FD3. W0 is three. where you going to read the bytes to you pass a buffer location in some address in memory address 10,000 hex whatever then how many bytes W2 100 bytes right so that's how you pass in parameters um in like C right that's what happens with with CC code and then the return value is W so it's very it's very simple and the way this program works is there is no push and pop ignore all this stuff about debugging it just goes right in and does what you wanted it to do there is no preserving registers all registers now it's the other extreme nothing is preserved. They're all destroyed. You have to assume that every register is obliterated by every function call, especially system calls. That's what SVC is is a system call. So basically in in ARM and I could even show you here um in ARM 64 there is no information. Look for yourself on which registers are clobbered. At least system 5 it said you know Linux said this is what we're going to clobber. This doesn't say that for this. So from what I could tell online, they're all fair game. Everything from X0 to X18 are all fair game for system calls and they could all be destroyed. So we have this helper function, this macro here called push volatiles, which basically pushes them all to the stack. How does that work? Basically, we we define a certain number of of bytes to save on the stack and then we just dump in these bytes in these locations, all the registers, including link register, and uh pop them off. So this is how macros work. I'm not sure we covered this before, but basically what a macro is is basically it just copy and pastes. So wherever you type in underscore un sorryore push volatiles, it'll just paste in these lines when it assembles this. So it just copies and pastes what you have. And then you could you could pass in parameters. So I could pass in like ABC. And then now whenever I type in /abc or slabc it whatever you put in there that could have been you could pass in x0. So let's say I type in you know push volatiles x0. Now wherever abc is in this code it will just paste in x0. That's how macros work. And I'm showing you that now because it's going to make it easier later. But that's how that works. Close out of this and go back in. So basically the easiest way to call something is just to not push anything. Don't push anything. Don't pop anything. Just run the program and deal with the consequences. If you want to see register, save it on the caller side. Make it a call or save register because that would be the most efficient way to do this in some in some ways, not always, but in some ways, especially for small uh programs. So I'm just trying this out. You can do that. You can just call functions directly like that. Just branch to them with the right inputs. That's that's the very simple approach. We have macros now. So speaking of macros, I have two macros for read. I have double underscore read and single underscore read. How does this this work? Okay, so some background information. The move instruction on ARM basically it can do two. Well, it puts stuff in a register. So here you can see move x20 and some parameters fd buffer number of bytes just inputs to the read function. So how can we use this? Well move can take in either a register. So I could pass in for fd. So I can say let's say read um three buffer location number of bytes right what does this do this basically passes in just immediate values right three is a number of file descriptor that's a value I know what the value is is three you don't usually know what it is by the way let's say standard output it's going to be like one or something you don't usually know what the number is that's going to be in a register so that'll be in like you know w0 x0 or whatever right so you could take in either an immediate value or register. And that's the cool thing about move is that it can handle both. So we can use move to almost make this macro like input agnostic, input type agnostic. Basically, I could pass in either a value 300 or a register x3, right? That's pretty cool. So I could pass in x0 or I could pass in zero and they would both theoretically work. It's not a value of file scriptor, I'm pretty sure, but it would work nonetheless. Okay, that's cool. So what's going on in this program or this macro? So again, this gets pasted into the to the binary basically as you read it. So what is going on? Well, we're subtracting off the stack 32 bytes. That's enough room to place some stuff on the stack. And in in ARM, just pro tip here, you always have to be 16 byt aligned on the stack. Even though we only put three 8 byt quantities, so 24 bytes, the stack needs to be 32 byt aligned. Just pro tip. you'll figure that out yourself if you don't know it. So, just remember that. Um, and what are we doing? So, this is very weird. And the reason why I'm doing this way is kind of simple, but let me show you what's going on just in general first before I go into the details. So, first we're taking the first parameter, putting it in x20, and then moving it from x20 to the stack at offset zero. Same thing for the buffer parameter, parameter 2, and the parameter um three number of bytes at different offsets on the stack. Then it calls another macro and then it fixes the stack and then it's done. So basically the idea is whenever you want to read from a file descriptor some number of bytes to a certain location you just type in underscore read the file descriptor number the buffer location number bytes kind of like in C. It's almost as if you're typing this out in C, right? Without the parenthesis and the semicolon. So it's very ergonomic. It's very easy to read, very, you know, pleasing to look at, I would say. But why all this nonsense about X20? Well, here's the thing. Remember the inputs for the read function are supposed to be in X0, X1, and X2, the first three registers. The problem is what if my um file descriptor is in X2? So, I'm going to pass in I'm going to say read X2. And what if my buffer location is stored in X0? And what if the number of bytes I have saved in X1? Well, now we're screwed because how are we going to get these things over? How how am I going to like, you know, understand that X2 has to go to X0 because remember I need X2 for here, right? So like uh it's going to overwrite itself, right? You know you know what I'm saying? Like if I have to pass in X2 to X0, well then X0 is now X2. And when I pass in X0 to X1, X0 was X2. And so everything is messed up. So we have to figure out some way in case some dunce aka me tries to pass in the same registers as the inputs are going to be how we can make it kind of always work. And I racked my mind with one of my friends and he he's like, "Yeah, why don't we just pick a random register and just use that to transfer things around?" So I said, "Yeah, let's roll the dice." X20 we picked God picked for us and we're just going to use X20. So now from now on if you ever use a a macro one of these underscore macros just know that you can't use X20. That's the rule. Why X20? Well, because the first 20 of them are taken by the SIS call. So I figure, hey, this one's a good one to pick. Okay, great. So that's the idea. So now whenever you want to call read on a on whatever you're doing so um you can do so just directly by calling the function as you would normally with you know move instructions and normally calling it and saving what you have to save right or you can call this read macro. But what does this read macro does is it calls this other read macro double underscore read. How does that one work? Well this one it this is a stack like input macro. Everything that you are passing into this is passed on the stack. It's kind of like web assembly, right? In that way, that way is is that stack like driven? I'm pretty sure something is driven by the stack where you get inputs on the stack stack machine or whatever. Anyway, this is kind of like that you're passing the inputs on the stack. Remember that's what this was doing, right? And then you call this macro. What else is happening though? First, so first this macro is here and I'm pushing all the registers that this function cloppers. This is very reminiscent of what we just did in x64, right? Where we pushed everything that was clobbered onto the stack. At the end, we popped everything off, right? And of course, it's not push and pop. It's, you know, store and load, but it's the same idea. So, that's very much the same thing. And then what's happening? Well, we're going to grab off the stack way over there past what we just pushed. So, it's offset by that volatile length, which is the number of bytes that we've saved to the stack. And we're going to pull off input parameters and we're going to pull them off into these locations x0, x1, and x2 temporarily because remember they're going to get restored by this pop volatiles at the end. That's the idea at least. So this will preserve all registers except of course you can see here again we've used x20 to save the return value because read the system call returns number of bytes that it's read. So if it reads 100 bytes, it prints 100 bytes, right? That's what it returns. And so we can save that x20, pop everything back where it's supposed to be, and then put it back in x0. And in some circuitous way, we have reimplemented what we did in x64, but with macros in a way that also allows you to call things directly to the to the function call with a branch. Or you can call with um a very like C-sque syntax. Or if you're really that nuts, you can push up onto the stack and you can call this one directly. That's kind of cool, right? Call the same function in three different ways. In fact, the second way calls the third way. So it basically it's almost incestuous, but it's in a good way because it's kind of nice the way this works. So I kind of like this convention. Um it's not universally better. In fact, I would say it's it's maybe even worse than the old one was. But it's fun to try new things and see how this stuff works. And I'm sure we're going to come across problems. I already did it with with this with this stuff. So, we'll see how we can fix those. And it's fun to fix problems. Um, at least in my opinion. So, we'll figure that out. It's very novel, you know. No one ever has this problem because no one ever thinks about this stuff. It's only, you know, you and me. So, I think it's kind of cool to do this kind of stuff. So, that's the calling convention. Let me go back to the slide to see what was next because I can't remember. Oh yeah, the debug logging. So, let's go back into that program and look at what is inside there really quick. And this might be a spoiler. Maybe I should just run the program first. Um, let's do that. So, let me go into example 2A. What am I doing? Uh, CD. And then, so hold on. First things first, I'm going to remove this log file and I'm going to show you what's in this run shell script. So basically, just to summarize, we have some code code.asm. We're assembling it into a temp file. We're pulling all the useful parts of it with all these lines here, and we're running running the file. So if I run this file, this binary, you'll see it prints out to the screen this random letters. What are those? Well, if you're curious um what's what's under um that test file. Oh, sorry. The same thing. So, this program is printing out file contents kind of like cat would um in a in a simplified way, but without any inputs of course. And it's doing something else as well. You see, it's making this log file. So, what's in this log file? It says what it's doing. It says I'm opening test file success. File descriptor was four. Trying to read 32 bytes from that file scriptor. Here's what they are. 12 bytes read. Tell me to write that many bytes to FD1. That's standard output. Here are the bytes. Exiting with code zero. What does that remind you of? Anything? It reminds me of S trace which is how I literally debug like all the software in the beginning because you have to like bootstrap stuff from nothing. Before you have a print statement, how are you going to debug stuff? You can only before you have a function that prints stuff out to the screen. How can you even print hi h highi high to find out where problems are in your code? You can't. You have to I guess what I do is I return exit codes to the shell, you know, like like that kind of stuff. Or you can look at the s trace where the seg faults are. That helps in some ways or the dssage. That kind of helps sometimes. But either way, you need to have something like this. So, if you're familiar with Srace, what does this do? This prints out all the system calls that you basically've done in whatever you've calling calling it on in order and with the parameters. So, it says basically, you know, you've opened the log file, you've written opening to it, you've written the test file name to it, you've written blah blah all this stuff, you've actually opened it, right? So, basically this is the debugging and this is the actual program doing its work. But it's the same stuff, right? We're remember we're writing we're reading uh 32 bytes. That's what we did, right? We're writing 32 bytes. Where's that? Right here. That's what we did. And so our implementation, whatever it's doing is mimicking S trace in what I would consider to be a more easily readable format. So if I again print the log file, you'll see it's the same stuff printed out in plain English with all the values you need to know. So it includes like what's going on, is it success or failure, FD numbers, this kind of stuff will will definitely help me debug in the future. So how is this working? Well, let me go back to read and I'll show you what's going on. So I was under what lib cis read. If you you probably saw this trash. Basically, there is a a flag called verbose logs. And if you set verbose logs, it will conditionally include this stuff. So basically this stuff only is in there if that flag is set. So if the flag is not set, it's not in there. So what's be included? It includes the ability. This procedure here is the ability to print text. And this is the ability to print decimal numbers conditionally included. So it's not going to be there if you don't want to print debug information, but if you do, it will conditionally include those things. What's this? These are a bunch of strings in memory that you probably recognize from that output just now. This is what's being printed to the screen. It's just some address numbers, um, some labels, whatever. And then under the read function call, there's a conditional block again only if you want to have debug information that basically tracks what's going on. So it will print out that text, that decimal number for the file descriptor, etc. just so you can see evidence of what's happening to help debug when things go wrong. So we don't have to use S trace as long as we can embed this conditional debug information to the log file. And so you can see there's one chunk that occurs before the actual read happens. That's like we're attempting to read this many bytes from this descriptor. Then we're going to run it and then there's some checks here to see um you know if it was a failure or not, right? or maybe not for read but at least for the other ones where there is a possibility of failure then it will say success or fail. So when you open a file it could fail when you delete a file etc. It could fail. So it will tell you for those but not for this one but either way you have information that you can log to a file or also the screen remember because the screen is also a file descriptor it's file descriptor one on I think Linux. So how is this actually implemented when we call the function, right? Cuz that's in the code. How are we setting this flag in the first place? So here is just some simple code for what's going on. And this is kind of like everything we just talked about with the macros and all that all combined into one. So we have some basic header as before. Nothing new there. We have some includes open, read, write, exit, whatever. Basic stuff. We have system calls as before. No problem. And we have two things here I want to talk about. We have this log file. S and we have this verbose logs flag. So this has how you set up you know a pre-processor whatever parameter in the GNU assembler the EQU the syntax is how you do that. So it sets the flag to one. It defines the flag. You can also define a flag from the command line um when you call the assembler itself. And that's actually better in many ways because you don't have to go into the code and you know comment out the flag every time. you can just change the the tac whatever you are passing in as a parameter to the function call itself. So that's kind of cool. And then also this this log files. So how does this work? Let's look at that first before I go to anything else. So uh where's that? It's under lib the cis log file. Is it zio file? Hold on. What's going on? Yeah. log file. So this is again um mo the this is basically all of what's required to store a file descriptor, open a file and store file descriptor in this location. So basically all this is this this whole include is nothing. It's not real cuz macros aren't real. They're just copy and paste. Remember they don't it's not actually code. This code just occurs whenever I type in log file path. The assembler will paste all of this in replacing this parameter with whatever you pass in in line when you assemble the program. So this is not really included. The only include here is actually this a single word of information. Was that four bytes where we're including the standard where the default is standard output file descriptor. So that's probably one or whatever. This basically stores where am I going to print out this debug information? Am I printing it out to a file? What's the where what is the file? So, it has to be opened and what opens it. If you don't open anything, it's the the screen output that defaults to standard output. If you do pass in a number, a path and you call this macro, it will then open that file and it will store that file scripter number in this location. So if I if you wanted to if if you passed in log file ABCDEF it will create a file. You can see here it has this the flags for cis create and cy truncate. So it will create a new file for you and it will dump the descriptor at runtime of whatever that file is in this spot. So if it's proctor 3 it will be three. It will the number three will be in this location. And now all of those log routines, they check for this value. They're going to write to whatever is here. That's how that works. So if I go back to the code, that's what's happening in the includes just that. And then here's the actual program. This is remember this is like a very simplified cat program. So how is this working? Well, we're calling that macro log file log file name. And again, that's just in memory. That's just a null terminated string. You can see here it's log.file file with the zero bite at the end. So we're creating a log file, opening it up with this macro and putting it that number whatever number that happens to be in that location. Then we have a open readr macro that opens a different file name. This is what we're trying to read this test file name. Then we are doing some mumbo over here. I'm not sure why. Reading and printing it to the screen. So this basically is a very primitive version of cat. Um and then exit with zero. Okay. So you can see our calling convention is very nice, very like concise. Everything is very clear what's going on. I'm not sure why this is here. I can probably delete this out. Yeah. Why is this here? Is that important? No. So that's garbage. I'm just I'm just dumb. So anyway, it's it's just that. And um it creates that log file. And again, that log file is very useful hopefully for debugging down the line. And it's conditional. So if I go in, let's show you this really quick. If I go in and I turn that off. I hope this works. Uh, it didn't work. Basically, this is not supposed to happen. I have another check over here. Let's comb this out as well. You can see it it's it still works. And now there's no log file. If I remove the log file, run this again, you'll see that it still works. the log file is produced. So, and now the file is way smaller if you see only 600 bytes whereas before it's probably in the thousands of bytes. Um, so yeah, that's that. Example two, example 2B, this is actually going to be real cat implemented in just our primitive syntax here. So, how does this work? Let's take a look at the code. It's all the same stuff. Now, we didn't add anything. We we replace the print statement with write because now we're writing to a file, not to the screen. But everything else is the same. There's no difference in any of this. And the only other difference in the logic is now we're not reading in a fixed number of bytes from a fixed file. We have to get the input from the command line, which is what's the file we're reading from. And remember from our days of old in x86, that comes off the stack. No difference. And you could check the number of parameters on the stack as we talked about before. Command arguments. go back and watch one of the old episodes on that. You have to check that first. Then you open the file and you're printing stuff from from the file name. You can do that. And then only other thing that's in this is that is basically we have a buffer. So um that buffer is much larger. And why is larger is because you can have files that are much larger now. So you have a file that is you know megabytes long. And you don't want to have to basically be pulling in 32 or 16 bytes at a time. You can pull in way more than that. And so we're going to use a larger buffer. But that if you're smart, remember this is in memory somehow. So you can either put that in memory in the binary, just have a,024 bytes here at the end of the binary, which is fine. You can do that. Or what you can do is you can put that in memory when the program is loaded and not in the binary itself. If you recall from the program header, there's these two entries, quad word entries. One of them is the size of the program in in data in the actual file storage right it's say it's one megabyte of data of the program itself of instructions and then you put in two two megabytes 2 million in this quad word this is the number of bytes when the program it's loaded into memory that extra thousand or million bytes at the end so the first million is the program the second million is going to be probably all zeros right initialized for your use but not for it's not actual instructions. And so how is this useful? Well, you can see here we have the buffer size added on. That basically means if this program is let's say it's 3 kilobytes in memory when it gets loaded into the by the kernel into actual runtime memory 1,024 extra bytes at the end are added that we can use for a buffer. That's how that one works. So that's one difference. And the only other difference from the old simple program to this one is that when you read a file, you can you have to read multiple times. So let's say it's a let's say it's three and a half thousand bytes. And our buffer is a thousand bytes. So you basically just have to read a,000 bytes, write 1,000 bytes. Read the next thousand bytes, write those bytes. Next thousand bytes, write those bytes. Then the last 500 bytes, write those bytes. Then you're done. So basically you just have the loop through with the buffer as your iterator essentially I guess I don't know very simple but the cool thing here is again you kind of can see how much how less verbose it is to be able to use these macros. So exit one is just returning one to the to the shell right open read write is very nice you can like reduce that down. So it's pretty cool stuff I think to be able to to call functions like this print buffer x0. I can print the entire buffer contents to file descriptor um standard output and this many bytes x0. So it's it's kind of nice to do that, right? So and again it creates this log file. So what's in the log file? Oh, hold on. What am I doing? Uh let me run this. So we now have a binary, right? If I run the binary on anything, it will print that to the screen. So if I print out, if I basically run the binary on the code itself, you'll see that the code occurs on the screen. This is all the code we just talked about on the terminal screen. Great. No big deal. And what's in the actual um blog file? What the heck is this? Oh, what? Oh, did I use the same one twice? Oh, I'll fix that later, dude. No worries. Anyway, you know what's going to be in there? There's a bunch of random stuff printing out to the screen what what we just did. So, I'll fix that later. Um, let's go into the next example. This is the example that I'm most uh proud of here because this basically replaces all of these lines of our shell script. Remember, we're using the assembler here to um assemble the code into the object file. And then this line and this line and this line and this line and this line and this line, all they're doing is sucking out the binary from the object file and making the new thing an executable. That's all it's doing. All of this. So, if we could implement that in assembly, it'd be way better. And that's the point of this is we're actually doing that here. And so we're going to basically create a binary. We're going to use that binary to create itself just to prove it works and um remove the old one. So that's the process here. And we'll show you that it's it's the same size and everything and it works the same. Last point about this shell script that I want to point out right now is the two lines at the top. These two lines, they're the same thing um with one difference. The first one just assembles the object file. The latter one passes in the the flag that actually turns on the debug information. So you remember I said you could pass it in with the flag in the code with the equ syntax or from the command line itself. This is that syntax here. So if I were to comment this out, there is going to be oh my god um no debug information. This one yes debug information. Got it? So we can see how that works. So I'll put this one here. Um, and let me comment down this line really quick just to show you something when this runs. So I run whatever this this program is. Um, I have those two binaries. If you look, they're both the same size. 4529 bytes. Most of that is debugging garbage, but they're the same number either way between them. And if I do diff, just to make sure, diff, that that does it compares stuff. Yes, they differ. If I pass in the two binaries, they don't differ. They're the same. So that's good to know that our binary basically can produce itself. It's like a self creating function, our executable. Why is this good? Well, it's good because obviously we can get rid of all our dependency on xxd. Um, but not really because on my phone if I try to open up Termox and do this stuff, the XXD syntax is different. Um, I can't use the same things. It doesn't let you like certain things aren't allowed. So really, I have to package this binary with this soy hub repository so people can actually guarantee themselves that they can use this. Basically, I'm going to package an xxd in in the codebase that we can use um for this purpose or if you prefer and I would probably do this, you know, deliberately if you could is just manually um create this extractor yourself and I'll have the shell script to do that included as well. So, that will all be part of um the codebase when I upload this to the soy repository. So, anyway, that's that. How does this work? So remember all it's doing is it's opening that temp file, that assembled object file. It's looking for the code size. The code size is embedded right here. Once it figures out how big I have to suck out of the file, then it sucks that amount out of the file. It uses lsek. Remember lsek is the system call that moves the cursor inside the file when it's reading. You know, if you want to read from bite 123, you can lse exactly there and then read that bite. So that's how that works. And then unlink is just delete. It's just RM. Basically, that's how you delete files. And so we're deleting the old file. Remember that temp file gets whacked. So it's gone. Um everything else is is very not worth going through. Basically, it goes through it. It you know what you would expect. It's basically cat, but it it cats to a certain file descriptor that we are passing in. We're passing in multiple parameters. Now we have to pass input file and output file to come off the stack. as before. So all that is no big deal. The only thing I want to point out here is how I've wrapped the logging stuff. So you can see I've comboed out this line. This line is gone. The flag is no longer being set in the program. It's being set at the command line when you call the assembler. Then in the actual instructions, if that flag is set, now I want to include this to create uh to basically set a certain log file conditionally. So not always but only if I pass it in the command line. And then last at the very end that actual log file name and you could pass on the line if you could pass this as an input to the program as well. But I I say it's not worth it. Um just put it in in conditionally in memory here. Um and this will only be included again if you pass that flag or both logs as a as existent as valid as one. Maybe zero would work too if it's as long as it exists. I don't know. But we have to set that at the command line. So that's how that one works. So again, if I run this, we have those two B those two binaries the same size and they're both 4 and a half thousand bytes. That's great. Um, but what if I were to go into the shell script and then switch these. So if I turn off all that debug information, and before I do that really quick, what does the log file say? You can see it's all this garbage. It's it's reading a thousand bytes, writing 1,000 bytes, reading 1,000 bytes, th00and bytes, reading the remainder bytes, writing them out, blah blah blah, deleting the file, exiting with code zero. So again, whatever we did is now being dumped to the screen. Very nice. Right now, what if we don't want that? I can delete that. And now I can run it again. This time I run it. Now remember, I commented out the line that adds the debugation to the to the binary. So now the files are much smaller and you can see they went from four and a half thousand to 1,700 bytes. So way less still not very small in my opinion, but it's way less than what it was. It's one/ird, right? That's great. And it still works, but there's now there's no log file. And if you were to dump out the binary or the other one, both of them work, there's nothing in here about attempting to read, attempting to write, whatever. It's all useful information. However, look look at this. Do you see something familiar? What is this? CH KI 3FG all over the place. What is this? I have no idea what that is, but if I had to guess, that is all the random macro bloat that we put in there. All those pushes and pops and move into X20, out of X20, onto the stack, off the stack of all tiles, all that crap. It's probably this. It's over and over and over again, right? That's bloat. Remember when you use a macro it literally pastes your stuff in line when we had our old calling convention it didn't do this remember it just this was in one spot only this was in the actual address of the function itself we had pushes and pops and that's it nowhere else now whenever I call a function if I call you know read in 10 different spots in the code not not if I loop through but if I if I call read read read different ways it's going to be pasted here multiple times which is not ideal um from a file size perspective or from a you know speed perspective either that's a problem but not really if you just you're trying to make a program honestly it's only a thousand bytes right who I mean who who gives a crap it's so small no one cares anymore these games nowadays are like 100 gigabytes whatever so this is basically nothing but still it's just a matter of principle right so let's go to the other example so this was let me show you this was example We'll see that was extract binary. I have two other versions that hope to make this a little bit more efficient. So macro list, remember all this below I said was from the macros, right? What if we tried to remove the macros from this entirely? So let's go in there and see how that looks. So you'll notice the same includes as before, the same comment as before, the same headers as before. All that's changed is now I'm not using that underscore syntax at all. I'm literally calling the functions in the the simplest way possible. I'm just moving the values into the registers and branching with link to the function address for the start. That's it. So there's no there's no pushes, no pops. Everything gets destroyed. So you probably want to put stuff on the stack if you have to. Things you need need to keep safe, you'd save them. Maybe you want to put things in memory. to save memory, whatever you do. Either way, you can see there's no underscores anywhere in this except perhaps maybe the the log file one, which is neither here nor there. So, how does this one perform? If I run this shell script, what do we have? A binary extractor, right? So, hold on, let me show you what's in there. So, again, it's same thing as before. We've um let me put this one on so I can see the sizes before I get rid of the debug information. Um it's all the same. Lead out the binary. Who cares? Not a big deal. If I run this, what is the file size of this new binary extractor? It's now 3,000 bytes. Remember, it used to be 4 and a half thousand. Now, with just without the macros, it's 3,000. So, I still have debug information here. I'm still making the log file, right? If I cap the log file out, all that stuff is still in there. So, I'm still making this, but the file size is now smaller. So, the the macros is almost the same thing as it's not as bad as the debugation, but it was it's significant, right? So, if if I go now and I change that line to get rid of the debug information and the macros. Now, how big is this file? 488 bytes. So, we've gone from 4 and a half thousand bytes for a fully macro, fully debugged file. Get rid of the macros, it's 3.3,000. If you get rid of the um debuggeration, it's like 1,700 bytes. Get rid of both, 488. Okay, that's great. Now, what else can we do? That's the question. This is already pretty small. In fact, I would say this is the limit for most programs, but for very small programs like this one, you could actually go further. And you could do I'm not trying to be a super code golfer right now, but you could do a lot more than just this. But let's look at the last example already really quick. So, uh 2E this one is extract bin functionless. So, remember whenever you call a function, let's not use VI here today. Um if you call a function you have to branch there and return back. That is four bytes for everything. So for everything is four bytes in ARM. So four bytes to branch, four bytes to branch back. So that's that's essentially for a small program you don't need that. You I don't have to branch to an exit call. I could just implement exit here. Right? And of course for very small functions where you're only calling every every procedure once, this makes sense. If you're going to call procedure 100 times, obviously this is but you know, if you're calling exit one time, you're going to call read one time, right? It makes more sense to just embed stuff in than it does to actually jump to the other other memory location and execute there and then return back. Also, it's probably way faster to avoid having to branch, right? doesn't really matter so much when you're you're probably sis call limited anyway on speed but either way this program should be smaller that's the idea and if I run this and look at the file size indeed now it's 476 so we only saved what was that 12 bytes but you can see how for very small programs it doesn't make sense to include any files because all it is doing is very basic stuff so hold on Uh you can see in the includes there is nothing. All it has is the system call listing which is just a list of all the sys calls and what they are. So yeah that's how that one works. And I would say out of all of these, I would probably I'd probably prefer the second one we just did. Um, example uh D extract bin macro list just because this one gets a little bit unwieldy and I feel like this is kind of deceptive. I I think I'll sacrifice extra 12 bytes just because this one looks prettier in the um in the actual code.asm procedure. So yeah, I think that's nice. Uh, I guess that's it. If you guys have any questions about this, leave them in the comments. I probably know the answers, but if you do, that'd be cool to see. Um, this is fun, you know, to try new things in new ways, different constraints, similar problems. ARM is more geared, at least in my experience now with this stuff, more towards hardware, and so got a lot of plans on how to interact with different types of devices. It's already in work, but it's nice to have this debugging stuff and this macro stuff and all that colon connection stuff behind us when we move toward these more advanced peripherals. This way, we can add that to the debug information. Now, we can say, "Oh, I'm I'm writing to this device. I'm trying to set this LED, whatever." You can add that now to your debug logging. you can add those type of macros to those function calls and make this whole codebase um operate in a similar way to what you saw here today. So anyway, thanks for that quick check up on the clock. Um okay, good progress. Anyway, thanks for watching. Have a nice day. Good luck with everything.