Submind YouTube summaries
Thumbnail for ls in x86 assembly | SCHIZONE LAB-06

ls in x86 assembly | SCHIZONE LAB-06

Watch on YouTube

Video summary

This video demonstrates the creation of a minimal `ls` utility written entirely in x86-64 Linux assembly for Lab 06, aiming to replicate core functionality such as listing files with sizes and color coding while achieving a drastically reduced binary size of approximately 886 bytes compared to the standard 129KB version. The implementation relies on two primary system calls: `getdents` to retrieve directory entries via a `dirent` structure and `stat` to gather file metadata like permissions and size. To ensure compatibility, C code was initially used to determine specific syscall numbers, structure sizes, and bit offsets for flags such as executable and directory indicators, while the assembly program handles command-line arguments to distinguish between listing the current directory or a specific path. The core logic of the program utilizes dynamic memory allocation within the BSS segment rather than embedding buffers directly into the binary, employing nested loops where an outer loop opens the target directory and an inner loop iterates through entries using `getdents`. For each file encountered, the filename is copied into a buffer, followed by a call to `stat` to parse the resulting structure and determine the file type. Based on these flags, the program applies ANSI escape codes for color output—yellow for directories, red for executables, and white for regular files—before printing the filename, size, and appropriate suffixes until no more entries are returned. During verification, the speaker noticed a two-byte discrepancy in the binary size likely caused by an accidental use of a smaller instruction and discovered that running the utility on subdirectories triggered a segmentation fault due to incorrect bracket usage in a `strcpy` call within the buffer setup. Using custom debugging tools developed in a previous video, the error was instantly located and fixed by removing the brackets, which restored full functionality for all inputs. Once confirmed to produce output matching the standard `ls`, the speaker discussed potential enhancements such as adding permission checks for users and groups, accessing additional metadata like access times, and implementing alphabetical sorting before removing debugging code to finalize the binary at 886 bytes. The video concludes with the successful re-implementation of the `ls` command from scratch in x64 Linux assembly, proving that complex utilities can be recreated with a fraction of the original size while maintaining essential features. By stripping away unnecessary components and optimizing memory usage through dynamic allocation, the project highlights the efficiency and educational value of low-level programming. The final product not only matches the visual output of the standard tool but also serves as a testament to understanding system calls, data structures, and binary optimization techniques in assembly language.
Read the full video transcript
Hey guys, welcome back. Skit on series lab six. Today's video will be basically an implementation of a subset of LS in X64 Linux assembly. Not a huge task, actually very easy and you'll see how easy it's going to be. Um This is cool because LS is a very popular and heavily used tool. I mean, if you're a novice command line user like myself, 3/4 of what you type in there is LS, you know, you know, so it's like high volume usage tool and not that hard to recreate at least in assembly. So, that's today's topic. And the motivation for this is because basically, yeah, it's easy, but also question why is user been LS so big? It's 129k. And all it does is print out, you know, directory contents. So, why so big? Yeah, it handles a bunch of random stuff, right? Different flags, but there's no reason to be that big of a of a function, that big of a program. So, if you clone this repository and you run the make bins directory, uh and you go in there, um and take a look, there's a list executable, you know, list. That's today's objective is to make that function. And if you run, you know, if you run that function, list, it prints out what amounts to being basically LS {dash} {dash} L. Um so, you get basically indication of the file size, you get a list of the files, obviously, and then you get some color coding um as well. So, that goes into regular LS's as well. Yellow for directories, white for files, red for executables. But, you'll note here that our list program is only 888 bytes compared to 129,000 bytes. Uh yes, it can only handle a small subset of the cases, um but it's a very small and it's not even optimized for size. We could we could really improve that. You probably get down to like at least 500 or at most 500 bytes, no problem. It wouldn't even be a huge task. It most mostly just be making our print routines more efficient because they're now they're kind of bloated. So, either way, yeah, 888 bytes to recreate a part of LS. And yeah, there's no um date, you know, file access time here or whatever. You can add those things in, of course. If you're If you want to, you can add those in. They don't They're not valuable to me, so I didn't put them in my version, but if they're valuable to you, go ahead, add them to your They're not hard to create. So, first things first, some very basic very basic theory here. We're going to use these two syscalls for this. Or two new syscalls, I should say. One is number 217, that is the getdents. That basically returns a structure here, dirent structure, of which we want to grab this d_name character array, that's just a file name with a zero at the end. And then syscall number four, you know, a very important syscall here is stat. This gets file information. For this, we're mostly concerned with obviously the stat structure, which returns for us st_mode, which contains a bunch of bits that say, "Hey, is this file executable? Is it readable? Is it writable for different groups?" And then also you'll have this st_size. And of course, if you want to add more functionality for you know, the time it's been accessed or whatever, or you want to get the UID or whatever you want to do, fine. You can add those in as well. They're all in the stat structure. And so, first things first is Yeah, you get these structures, right, with these syscalls. That's the whole idea. You pass a file descriptor in, you pass a a number in, you pass a address where you want to put this structure when you get it. Shh. Fine, no big deal. You have to know, of course, how big structure is, how many bytes long this is, bytes long this is, as well as the offsets, how far in the structure is this character array. Cuz again, sometimes things are offset with padding, and you never really know. You can't really trust these numbers all that much in my experience. So, you have to kind of guess and check, which is what I did, or if you're smart and intrepid and a good C programmer, you can just figure that out with the C code. Um but yeah, we want to get basically the offsets for um st_mode as well as st_size as well as the bits of st_mode inside which we're defining if files are directories or not and uh if they're executable or not, etc. So, that's the key. And here is some C code you could just compile and run to get those informations on your system. Again, these are the numbers for me, again, on your kernel, whatever number it is and on your computer, your OS, whatever, it might be different. So, just run this just to make sure. Include the stat.h and dirent.h to get the structures. Um and then these things to basically query sizes and stuff and offsets. So, that's all pretty easy. Define these structures here for the stat and dirent. And then you can get the size of the stat structure with a size of. You can get same thing for the dirent structure here with size of. And then for offsets, so you get the offset of st_mode with an offset of function as well as for the st_size element as well as the d_name element of the dirent structure. So, use offsetof to get that. And then lastly, to get the bits for is it executable, for example, for the user group? You have this flag S_IXUSR. So, in this case, we can print out that in binary. We'll use that as well, but if you want to just use C code for this, you can just, you know, print out a conditional executable or not if that is one or zero. Similarly, for the ISDIR flag, that's basically, "Hey, is this a directory or not?" And again, we'll get that binary value as well. So, here's the output of that if you compile and run that. So, our stat structure is 144 bytes, torrent is 280 bytes, and then our offsets for st_mode and st_size are 24 and 48. And if you're curious and you wanted to check, you can get the size of st_size just the same. Personally, you can just get it from this. Not a not a big task, but yeah, you can grab that as well if you're so inclined. And then offsets of d_name, again, you can grab that in the same way. And then here for this bogus file, this HTML file, it happens to be a not executable file. So, it's not a directory and it's not executable. And for that, basically, the first, second, third, fourth, fifth, sixth, seventh bit of the 24th byte of the stat structure tells you is this a executable file or not. And then the 0 1 Sorry, 1 2 3 4 5 6 7 8. 1 2 3 4 5 6. Again, the seventh bit, in this case of the 25th byte of the stat structure, tells you is this a file or a directory. So, if it's a one, it's a directory. If it's zero, it's a file. So, we'll use this information to help us implement our ls alternative here today. So, let's do that. I'll copy this over. So, we'll go into our lab directory. I always put these lab videos together. What a lab video is is basically an application that we can make using what we've already made. So, just a synthesis of previously created assembly functions. Uh and so, here you can see lab six ls. That's today's video. We're going to recreate that together here. I'm going to copy templates into I'll call it list. I don't know. Go into list. Inside here, you'll have a run shell script. Um this just runs nasm on our binary. It runs nasm, generates our binary, chmods that to make it executable, and then runs the binary. So, it's just a test case. Essentially, it's like a makefile. Um and then you have our code. Go in there. It's just a bare-bones template. This just returns zero. So, what can we do to make this more interesting? Of course, all the includes. So, couple includes today that are going to need. We will need um Obviously, we have to open files. So, lib io file open. That's a requirement, of course. We'll also need a couple ways to print out stuff. So, printing out strings. This prints out a null-terminated string. So, print string. And then we'll need print int d to get the file sizes. And these two things, they have a a dependency on print chars. We'll use that as well, but because they're included by these, I won't bother re-including those. And then um strlen, we'll need that to get the length of our string. So, we'll include lib io strlen. And then I think we also need Have my cheat sheet in front of me. We'll need strcpy. So, we'll include lib mem strcpy. But not just strcpy, we need strcpy null. I think that copies the null byte as well. So, copy null-terminated strings with the null byte. That's all includes we have today. So, uh not all that involved, you can clearly see. And if you wanted to, you could wrap those syscalls in functions. I'm not going to, but you could. Feel free to do so. So, first things first, we're we're making a program like ls. So, you have to get user input. And so, if you think about it, if they pass in nothing, they're just like an ls, hit enter, it's it gives you the current directory. Right? That's the the biggest thing. Um and but they also can pass a the a path. So, they can say LS you know you know, home, your username, whatever. So, yeah, so in that case, you have to handle either one argc or two argc. So, argc of one basically means, "Hey, no arguments, just the the program name." argc two means means there's one additional argument, which is a path. And so, we have to check the number of arguments passed in with you know, which is in argc. And then see, that's easy to do. It's also easy to do in assembly cuz we have a kind of a definition for what this is on the next in BSD. So, we define that um argc location basically is at sys argc start pointer. That's either rsp or something else, I forgot. Um but yeah, this should basically means at the stack pointer essentially um grab that whatever that byte or whatever it happens to be. In our case, I'm just going to compare the bytes at the at that address with the number one. And um if we're above that, I'll say we have multiple inputs. And if we're not, we have one input, so then we're good to go. So, uh in that case, we'll have to handle like um LS on the current directory. Or what should I say, working directory, I don't know. All right, so then we'll have a multiple inputs down here. This is basically if, "Hey, yeah, we have multiple command line inputs to worry about." Okay. So, if there's multiple inputs, we probably want to check that it's it's exactly two, cuz if it's more than two, this whole thing is is a scam. So, um you can't Well, I guess you could theoretically say LS multiple directories, but we're not going to do that. We're just going to do it on one single directory. And And no flags. We're ignoring all flags. We're just going to assume I always want to see the file size and etc. So, we'll just have have that here. So, we're going to check for exactly two inputs. And to do that again, we can just basically do the same thing. And this could be worked differently. You could do this in the very beginning, you know, you don't have to break this into two pieces, but if I just compare this with two, um I could say jump uh jump not equal to invalid inputs. And I can have an invalid inputs down here. If I spell that properly, then we'll we'll jump to you. So, then now basically, if our program gets to this instruction down here, we know that it had exactly two inputs. So, it had LS and then uh path. So, that's good to know. Um so, how can we do this? We'll need a couple of buffers. So, let's make the buffers first. Um we can do them at the very bottom. So, normally I don't do this. Normally I put them in the actual binary. So, I put them in the So, when you compile something and you make a binary, there's a there's instructions and there's data inside there. But also, when you when the OS loads it into memory, you can load stuff that's not in the binary. So, for us, you can see here, the file image is code size. That's how big our code is. That's you can see here. That's code size is um end minus basically the start address essentially. So, that's basically right here to right here. That's how much code there is, right? All the code we're writing, all the instructions are going to be between the elf headers and the end of this image in in in memory. But then, if you want to include additional bytes when the program is loaded, you can do that here. So, when it's loaded into memory, you can add additional bytes. In this case, we add some extra bytes for the print buffer, but we can also add some extra bytes for other stuff. If you recall, we had 100 What was it? It was Let me check. 144 bytes for the stat structure, 280 bytes for dirent, and let's just say um I don't know, 512 bytes for another buffer. So, we'll say 144 + 280 + 512. Give us some extra bytes to work with in memory, and we'll put those all down here. So, after the print buffer, I will just create some basically macros that define addresses of things that aren't going to be in the binary, but they are going to be when the program loads into memory. So, we'll say um buffer equals or not equals EQU um print buffer plus print buffer size. So, this basically says, "Hey, yeah, when the program loads into memory, this buffer is not in the binary. This print buffer is not here. So, just create an address that we can access in our instructions that just is offset from the print buffer by the size of the print buffer. So, basically, I'm allocating some number of bytes, how many bytes? 4096 bytes to the print buffer, and then after that, we have a just a another buffer here. And this buffer is 512 bytes. So, besides this, we need a dirent structure, so I can say um let's call it dirent struct that equals buffer plus 512. Extra bytes for the buffer, that's just going to be We can use that for getting getting names and stuff. And then we'll have a structure address for our stat structure, and that's going to be what it was 280 bytes from dirent. So, we'll say duran struct plus 280. And then of course, the stack structure is 144 bytes long, but we don't care because we don't have to define anything else past that. If you had another buffer here, if you had like, I don't know, cringe buffer, you'd say that was, you know, stack structure plus 144, etc. So, we'll have space in memory for these things, and if we refer to buffer, duran struct, and stack structure in our program, they'll be offset appropriately from end of the file, so we can properly access these things from memory, even though it doesn't exist in the binary. So, pretty cool stuff you can do there. Kind of like It's kind of like the BSS segment essentially for Boomer assembly. So, that's how that works. So, we'll have those we can use. Okay, so how should we get this to work? Um how about while we're at it, let's put in some other things down here while we're down at the bottom. Let's put in some colors. So, if you recall from our video on like ANSI color whatever stuff, um we had ways to do this stuff. So, we had a for like bright yellow or orange or something, the sequence of bytes the sequence of escape code was uh for me it's it's the escape, and then it's 93 M, I believe. Then for for red, so yellow is for directories, red is going to be for um executable files. So, that number was 31, and then for reset, that should just be zero, I believe. So, again, to get to get it back to white for regular files, you have to reset the color. Also, when the program ends, you have to reset the color, right? So, that's just I think I think it's zero M. So, we'll use all those things to kind of control that um the coloring of our output. And what else do we need? We'll need some grammar. Um so I'll I'll pick some grammar down here. I always put grammar in these types of things. So I'll call it grammar, then I say DB. And we have to have a couple things. We have to have a B for bytes. We'll also need um a new line after every byte we have a new line, right? So file name {dash} bytes new line and we'll also need a dash for in between the file file name and the file size. So {dash} um Besides that, couple other things, we'll save those for later, but for the most part, this is all the requirements we have for kind of data in our program. Okay, so once we're in once we have exactly two inputs, what is the first step here? So well, I guess the first step is going to be um copying our path into the buffer. That's kind of the biggest thing. And remember, the user can pass in a huge path. They can pass in like, you know, however long the maximum string is. I don't know, it could be infinite for all I know. um into our function. And so how can we get that to work? So that's where our buffer comes in. That 512 bytes says, "Hey, user, you better pass in a path that's less than 512 bytes long." Um that's going to be the case most of the time, but if not, the program will break. So let's copy our path into the buffer so we can use it uh in different ways. So we will move RDI. So we have a our stir copy, I should say first. So stir copy null. That copies a string and that requires a destination as well as a source. So usually you put the destination in RDI. That's what D stands for. So say move RDI buffer. And again, this points to that memory that we're it's not actually in the binary, but it will be there when the program is in memory. Uh and then we'll move it to RSI where we're going to grab here, and that's going to be the actual second argument, and that's going to be at sys argc start pointer plus 16, I believe. Yep. And why is this? Well, it's because argc is on the stack first and then argv. And then, of course, the first element of argv is just the program name itself, and so that would be offset eight. And so, offset 16 is the actual input that you're typing in. So, if you typed in ls home, this is the string home with the null byte. So, if I run this, this three instructions basically copies, you know, the path null byte to our buffer. But now, there is a little bit of a a question here, and that is if the user doesn't pass in anything. Because if they pass in no buffer, we have to pass in no path, we have to still have a something in the buffer, right? So, because we're going to use this buffer for processing later down, and so we have to have some way to suggest that there is still a path if there's no path passed in. And so, we'll just define that here randomly uh when the program starts. So, we're going to basically move into the buffer. Um I guess it would be two bytes, and those two bytes are going to be basically the dot slash, which means current directory. So, first things first, program starts, immediately load in that into the buffer. So, current working directory dot slash. Okay, that all works. And then, the question's going to be um slashes. Now, here's the thing, right? I didn't actually know this, but you can have as many slashes as you want. As long as you do that, like it still refers to the home directory. And you can also have as many dots as you want. So I can say that. Now like slashes have no purpose. And so if the user passes in like LS home it's also the same thing as like home slash, right? And so we want to always append a slash. I guess that's the idea here. So we'll do that. Go back in the code. And we will just append a slash to that buffer. Because if they pass in home with no slash, it's not going to be the same. We want to get everything inside the directory. So we always want it to have the slash on the end there. So I'm going to move Um well actually the first thing we should do is we should get the the length of what we were just working with. Because they could have passed in anything, right? They could have passed in I want to add a slash. That's the idea. So question is where do I put the slash? It has to be after the last thing in our buffer. Right? So home no byte, that no byte should be a slash. That's the idea. And so how can we make this work? Well, get the stir length. So we'll call stir length on um this. And actually so stir length you call it on RDI, which is the string address start. So already the buffer is in RDI and so we can just call stir length right away. And then now in RAX we have the length of our buffer essentially. So then um the question is going to be Hmm. I think what we'll do is we'll have another byte down here. Maybe a word. Could be a be anything. Could be 64-bit program lab. Let's try to make it small. We'll say um buffer offset. Do I have this in the example? I do. Yes, so I have buffer offset. This is going to be an offset into our buffer that we're going to save. And so I'll say DW two. It has to be two to start off with cuz that's going to be the the default buffer offset for if you had just this, right? So basically what we want to do essentially in this program is append to our path every file, run our stat structure on all those files, like you know, run our testing functions on every single file in this directory. And so I want to know the offset in memory for in general the next free byte in our buffer. And so for that, I basically want to start off with two, right? Cuz basically it for the for the case there's no input, you just hit an LS, hit enter, the path is dot slash. And so in that case you really want So this is offset zero, this is offset one. So the first byte of next file name that you see is going to be at the second offset. So that's why it's two. And so what I'll do is I'll call strlen, then I will um It It returns in RAX. So however long our our path was, 100 bytes for example, 100 is in RAX. And now the question is um let's now set our offset to that value. And so actually we'll offset by one more than that to get it cuz strlen returns, you know, the address of the last the number of bytes. So the offset is going to be one more to the next one, right? So we'll increment that value. Um then we will And this by the way, it could also probably just be an AL if we're going to allow it, but who cares? So, increment RAX. And then we'll move that value into the buffer. So, we'll put that in buffer offset. Um AX and then uh we will put a slash over the null byte. So, we'll go back one. We'll decrement RAX and then we will um So, this is just a number. We want to offset into the buffer. So, we'll add RAX plus the buffer. That will basically give us an address in memory where we can write the slash. In that case, I will move byte into RAX. The uh the byte value for slash What is the byte value for slash? Let me check really quick. I know what it is already. I'm just checking for your sake. So, for a slash, the byte value is 47 hex or 2F hexadecimal. So, we'll put in 47 there. 47. Okay. And then in that case, the last thing to do is going to be resetting RDI cuz basically at this point, we've handled the two different cases. We've handled no inputs as well as one input. And so, I want to rejoin our friend over here and so I'll have a um I will have a skip in address that we basically skipped in from the top here. And before that happens, I want to set RDI differently between for the two different cases. So, um the RDI for examples where you pass in a path, that's going to be actually the path again. And so, I'm just going to take this little guy and reset RDI to that value. And then in the case where we only had one input, then the question is I'm going to always put it below this comments. Um we will say move into RDI. And we could do the buffer again, actually. We could just say buffer, but I won't. I will I'll give it its own address just to be consistent. Um we'll have another thing here. We'll call it dot. And uh and we'll jump to to skip in. So, we'll define dot right now. It's just going to be some bogus um It's basically going to be this in memory. So, we could do that anywhere. I'll just do it right here. I'll say um What did I do last time? I said dot. Uh DB dot. See, this is our our file name currently. And uh put a null byte at the end, obviously. So, we have that. So, basically, the idea is once we're in skip in, we now have RDI. So, we have everything kind of set for the buffer. Buffer has the path. Our current path is now set in the buffer. Either it's a dot slash or it's just who knows what. But, you know, whatever you passed in it with a slash. Either way, it's a a path a official path that you can append, you know, file names to essentially. Um but then, it's for both the case of no inputs, it has a dot slash in the buffer. And for the case of your actual path, it has the path in the buffer. But either way, when you get to skip in, that is set accordingly. But also, you have in RDI the actual kind of path name. And so, what do you do first? Once you're actually in here, now you can open that directory. And so, file open, you're going to basically call file open. And this takes certain inputs, obviously. RDI is the file to open, so that's that path that we set in RDI, but also we have to set some flags. So, the flags go in RSI and RDX. So, we're going to open with um sys read only. We're not going to edit anything here. So, that's our permissions flag basically. And then RDX is going to be the permissions. So, we have the sys default permissions. Um so, yeah, read only is like the mode. So, there's read only, there's like create, there's read write, truncate, etc. We're just reading today. And then permissions is just default. We'll leave that at default. Then we call file open. And then now in RAX is basically a file descriptor for the directory that we just opened. And so, we'll put that directory in a register. I have to save things in R15 whenever I get them, so I'll just put it in R15. And we won't we won't ever clobber R15 if we don't have to. Okay. So, now we're down here. And um we have a file descriptor, hopefully. And of course, we could check. Maybe we should check first if it's a valid file descriptor. That might be smart. I'm not going to check, but you could just as easily check. Um you could basically, you know, compare RAX with zero. And then if it's less than zero or equal to zero, you could um jump to invalid inputs again. I'm not going to do that, but you could do that if you'd like. Ultimately, it doesn't matter. If it's an error, it's an error. We don't really care about giving an, you know, an error. It's just going to not work. So, yeah. So, what's what's the first thing? So, here's the thing. We have to have two loops. One loop is of basically getting the dirent structure. And the other is basically getting the file data. And so, we'll need two loops. And so, we'll have an outer loop here. I'll say outer loop. And we'll have an inner loop. I'll call that one loop. And we're going to basically jump to these labels in a loop fashion. That's usually how assembly language works. Um there's no you know no while loops or for loops. You have to kind of do it yourself. Um and so what's the first thing? So we're going to call a syscall for getdents. So that's going to be a syscall instruction. And the RAX syscall ID is basically we have it set to be um sys_getdents. And that is basically in the syscall's ASM listing, it's defined for Linux right now. I'll add it to BSD at some point. So sysdents is in RAX. And then um we will put the file descriptor that we're trying to get the dents for, which now is in R15, so we'll copy that over into RDI. Then we will put the address that we want to drop our direct dirent structure into RSI. Again, that's the dirent struct that we just set memory for down here, remember? So we can just refer to that space. Say, "Dump this structure at that location in memory." And then how many bytes is this? Well, it's at most 280 bytes. So we will just put that in. You can see the syscall. And now hopefully we have a dirent structure in here. And if you wanted to, you could add a you know a a thing here that said, "Hey, it didn't work." Leave. We will do that. We'll just compare RAX with zero. We'll say jump less than or equal to Now we could say invalid input. We could say leave. I'll just say leave. It's the same address. We'll put that down here. So either way you now have left your your little friend. So that's that. So if you have a valid input with this dirent structure, now you can start thinking about um looping through. And so, the idea is basically that we want to copy basically what amounts to being a like file name into our buffer. That's kind of the the idea here. And so, what do we do? Um So, if you recall, our file name was at offset 19 from the d_rent structure. So, 19 bytes in, our file name starts. And so, we have to basically get that onto our buffer. Remember, the buffer currently has the path to the working directory essentially. Um followed by or what I shouldn't say that. It's like the relative path or whatever followed by we want to put in as a file name. That's the idea. And so, we'll grab the file name out of the d_rent structure. Right? Let me scroll up for you to see that. So, there there is this character array here is in this d_rent structure that we're just we just got. And so, we will grab that first things first. And um yeah, so we will basically try to dump our file name onto that buffer. And that's what this inner loop is for is we're dumping that in. And so, what we'll do is well, actually we will save this um number of bytes into somewhere in memory. We might need that later, so I'll save that. I'm pretty sure we need that later, so I will put that in um RBP. We'll save the number of bytes. Save in RBP. And then, I have here we will basically uh grab our We're copying from one place to another our file name, and so I'm going to copy part of that source into RBX so we can use constantly cuz I don't know where it's going to be necessarily um down the road. So, we'll move into RBX the d_rent structure address. Is that required? We'll figure that out in a second. It might not be. If we can optimize that out. No, I think we need it actually down the line down the line. So, we'll keep that in there. Okay. So, that keeps us prepared for our inner loop. And now in our inner loop is going to be some um just basically move. So, we're going to have a stir copy here. So, we'll basically say um call stir copy null with some inputs. What are the inputs? Well, um in RSI, you need that d_rent structure offset to the file name, and so we'll move into RSI basically what's here is RBX essentially. That's going to be the d_rent structure's our address, and we're going to add to that just 19. That's basically the offset to d_name the char array. So, now RSI points to the file name. And um RDI should point to uh the buffer. And so, I'll move RDI buffer. And then we have to offset into the buffer some amount as well, and so we will add And we can just say DI cuz it's going to be a just a single word. So, we'll say add DI with the buffer offset. So, that's either going to be two in the case of your initial no inputs passed, but it also could be a No, whatever you whatever you get out of this. So, whatever your stir length was, you fasten some long path, it would be much longer number than two, obviously. And so, now RDI basically points to our first free space in the buffer. RSI points to the D name, and now we're copy now we're just uh running through stir copy null. This copies a null terminated string from the D name, which is what it is. It's just a null terminated string of the file name on top of the buffer, but only after which uh offset value you had nothing, right? And then I'm going to save RSI into R12 while we're here. So, just cuz we can refer back to this more easily later. So, move R12 RSI. Call stir copy null, and then we can go on with the rest of this. So, now we have a buffer which contains a full path and a file name, and now we can call stat on that. Cuz stat requires an actual file name with offsets to give you the information about that file, obviously. So, now we're going to do the other sys call, which is the system stat call. Again, that's defined in the sys calls listing. So, we'll say move RAX sys stat. Move um RDI back to the buffer. Move RSI the um stat structure address that we just defined at the bottom of this code, and then call sys call. At this point, we have a stat structure in RAX. I'm actually going to go to this new line here. So, we now have that in RAX. And again, you could check is this a valid return or not? I'm just going to say ignore checking. I hate checking stuff, cuz if it if it fails, it fails. Um of course, if it's an important check that is like a requirement that the rest of your program needs to run, you have to check that. If it's a matter of, "Hey, it's an error. It's an error. Just leave." then just leave, right? I I don't care if it errors and exit. If If it exits with an error or if it just gives me a nice message, it doesn't matter to me. The program didn't work. So, just end it however you want to end it. Um seg faults are fine with me. Whatever. Who cares? So, what's next? Now, it's going to be a matter of actually printing the stuff. And here comes the actual testing for the actual bits and stuff cuz we have now our whole stat structure. We have this now in memory. So, how do we get this to work? Well, we have to just offset in this number of bytes, 24 bytes, this many bits, and just start checking stuff. And so, in that case, we're printing, and so I'm going to put print stuff now here. So, I'm going to move into RDI our sys standard out. That's our terminal output file descriptor. I'm just going to start printing stuff. And so, I'm going to just grab different bits out of memory, and we'll start checking. So, we will check first to see if something is a directory or not. So, if you remember, that is going to be in the stat structure, but where in the stat structure? So, we're going to grab a byte out of memory for this. And this byte is for I think it's if they're a file or not. So, directory or file. It's what we said was the 25th bit. So, 24th bit is for the st_mode. So, in the stat structure, if you go 24 bytes down, what am I saying? Bits? It's bytes. 24 bytes into the stat structure is st_mode. And then in st_mode, you have to look this many bits deep to get if it's a directory or not. And so, this is, you know, one byte plus the seventh bit. So, in actuality, it's the 25th byte offset from the stat structure, and you check again that bit. So, let's do that. So, we'll offset the bytes from stat structure. Move that into AL and then we'll test AL against What I have here is 01123123. Again, that's the seventh bit. And if it is non-zero, it's a directory. So, if we've determined that that bit in the stat structure is now one, we'll jump to some other label down here. We'll call it dir and that will be uh we'll process the directory. Now, what next? Now, what if it is a um executable file. So, again, it's a very similar logic here. I'm going to copy all this. We're going to get the byte from 24 bytes offset and again, I think it's the seventh bit. And if that's set properly, we'll say jump to Well, actually, no. What we'll do is we will say if it is zero. So, not non-zero, but if it's zero, we'll say this is more efficient to do it this way, I think. Um we'll just say um normal. Or keep printing. Continue printing. We'll say that. And that will be down here. That's going to be a a white a white thing to print. Continue printing. Okay? And um and then if it didn't evaluate to this, so what we're checking here basically is is the is this IXUSR flag. Um again, this is the seventh bit offset from the 24th byte offset from the stat structure. And so, we're checking is this an executable or not. And so, if it's doesn't evaluate to a zero, then it obviously was executable. We won't jump to this. It just falls right in. And um in this case, we will say um move. So, you want to make it red now. So, we'll say move RSI the red value. The number of characters to print out for that ANSI formatting is uh five. So, 1 2 3 4 and then {slash} e is five. So, five and then 1 2 3 4 for the reset, I believe. So, we'll do that. So, basically the idea here is just to print the formatting. If we determined it was executable, it should print out red. If it's a directory, it should print out yellow. And so, that's the idea. So, we'll move RDX five, five characters, and then we'll call print chars. RDI should be set already to the output file descriptor, so we don't have to set that. Just call print chars, and now red formatting should follow this. Whatever you print after this will be red. So, that's this. Then we can jump to, I believe, the continue printing flag. So, no formatting further required. Now, if it's a directory, so if you've gotten to this directory flag, then we will move yellow into that address. What have I done? I hit the world. Okay. And then, no jump required, it will fall right in. Is that correct? That is correct. Okay. Now, we print the file name because we we now have the color printed properly. We we know everything else. And so, now the question is what what do we print out? We print the file name first, then a dash, and then the file size, and then the letter B, and then a new line. And so, we'll do all that right here. So, first things first, we print out the file. Let me save real quick. Print out the file name. So, I'll put some comments here. Print file name. So, now we move RDI already set, so move RSI the value. We actually saved that value in R12, thankfully, if you if you remember. We saved our offset to D name. So, now we're printing out literally from the direct structure. We're not even bothering with um anything else. So, we're printing out from the direct structure the file name. And uh yeah. So, we'll then just call print string. That'll print out the string. Then, we should print out um I guess the reset color. We set that to basically turn back to white. And so, print that out. I will just copy you. We'll say reset color. Address is reset. Number of bytes is four. Call print cars. Okay, now everything is going to be white. And now it's a matter of putting out the dash. And so, the dash was at grammar offset of 0 1 2. And print out three bytes. So, if I print out basically the file name in the color and then a white space dash space and then the file size, then a B, then a new line. And so, we'll do that. We will say I'm just going to copy this copy this guy again. We'll say print basically uh space dash space. That's an offset. We just said it was grammar plus two. How many bytes was that? That was three bytes. Call print cars. Then, the file size. Okay, so for the file size, remember we got this file size from our C code. This was at offset 48 in our stat structure. So, I'm just going to grab that. I'm going to move RSI. And we could even say um it's going to be a quad word. So, we'll just say stat struct plus 48. This takes the file size out of our stat structure offset 48 bytes from the top of it, puts it in RSI, and now we can call print int D. And now our file size is just printed. At this point, I'm just going to say put a comment here. We'll say print file size. And then we'll print the last bit of grammar. This time it's going to be Oops. Printing out basically what amounts to a B and then a new line. That's at offset I believe it's offset zero. And it's two characters, right? Let's double check. Yep, B new line is it offset zero? It's two characters. Is that correct on my list? It is. Okay, and then um we're going to flush the print buffer. Flush print buffer. Again, it's buffered printing. So, we will say call print buffer flush. And at this point, you could probably test this, but we have more loops to wrap around. So, this wouldn't evaluate very interestingly. It would just give us one. So, let's finish our loops and then let's test this out and see if I made any mistakes in copying this down. It should make sense though. It's not Nothing here is complex. We're just calling these syscalls, populating these structures with the syscalls, and looking into the structures to find useful data, parsing that data, and printing it out to the screen in different ways. So, it's very straightforward, and it logic is is pretty simple. So, if we finish printing, we have to now check if our stuff is completed. And so, um our inner loop now is basically a matter of grabbing the next item here. And so, we saved dirent structure into RBX at the beginning. All right, right here. So, now I'm going to grab the next element cuz if you look at the dirent structure, it has something in there. I should probably mention this. Um you can see offset to next Linux dirent. So, we saved this address in RBX, but now there's an offset to the next one. So, we can just grab that. So, we'll do that. So, we will I have here move RAX the word RBX plus 16. And we'll zero extend that. Uh and we'll add that to our current RBX. This is an offset value, so we're just going to add that here. And we're going to subtract that off of RBP. Let's explain that right now. So, RBP is set to the return of this Durant structure. So, yeah, that's that makes sense to me. And then now we can compare because if we've now got zero, we should fall out. Cuz it means there's nothing left to to pull and so I can jump non-zero back to the top of this inner loop. So, like that. That's the inner loop termination condition. Now, the outer loop condition is going to be um basically I think it just goes forever. Right? Cuz we're calling this on every single thing until we've hit the end. And so, right? Cuz we have the condition here at the top that says leave if it doesn't get anything, right? Yeah, right here. If nothing was read, then just leave. And so, I'm just going to jump back to the top of the loop. Cuz it we we can exit the loop inside. We don't have to exit the loop here. So, I'll just say jump outer loop. Outer loop. So, yeah, we'll never actually fall into this exit condition naturally. You have to jump to this. Cuz you can never get there non-linearly. So, I say we try this. I probably made a whole bunch of mistakes. Let's see what happens if I just leave this and I run. Well, obviously, there's no mistakes made. And actually, this is even less bytes. I wonder why it's even less bytes. Let's double-check against LS. Let me Let me clear this first off. I'll run I'll run LS L on our current directory. We have a binary 888 byte 886 bytes. Code 5,010 and our run is 172. And if I run just the binary, we get those same numbers. 172, 5010, 886, plus we get the the dots. Um and yeah, everything's colored correctly, red for executables and white for regular functions. Now, why is it two bytes less? I probably used a smaller instruction by accident than I had in my example today. But yeah, either way, the same functionality. Now, this is the local directory. Does this work on other directory? Let's just make sure. If I run the binary on user Let's just do home. Let's just do um a relative path. So, we're in skitson now. Let's go into the lib directory. Let's go into math. Let's go into expressions. Let's go into trigonometry and enter. Seg fault. So, there is an error. What is this error? Let's see if we can find out what this error is. We can use our debugging tools to figure this out. That could be fun. Why don't we do that? So, I'm going to Oh, you know what? Yeah, let's do that. Let's include our debugging tools and let's see where this segfault is. Just to show you how we debug this kind of stuff. Probably it's that two bytes that I messed up that is causing this problem. So, um lib debug debug.asm Let's run. Okay, it works with no inputs. If I run the binary with any input though, Come on, dude. It segfaults. So, let's see where that is. We have a function for that, find segfault.sh. It has an error between the loop and strcpy null. Okay, it's in strcpy is the is the error. So, it's probably in what we set up for strcpy. Let's check that. Go into our code. Let's go to strcpy null. What is wrong about this? Move RDI buffer. Oh, there we go. This is obviously wrong. Nothing is at that buffer address. It It's a It's an address, so you can't do that. You have to get rid of the brackets. I'm so dumb. Let's try again, binary on a directory. Now it works. So, yeah, that's a good example, because now let me test the other one, our uh trigonometry thing. That worked too. So, also let's test LS on that one real quick, just make sure it's the same results. Let's see. 3703 3164 2768267 Yeah, whatever. So, it all matches. Everything there is correct. And that also was a good example of how to use the debugging tools that we made in our previous video to help. Like that would have taken me probably 10 minutes to find, but that shell script found it found the problem instantly. And it's a very simple script, and if you're curious how that works, check the previous video. So, with that out of the way, we do have a working um LS alternative. Yeah, it's less full-featured, of course. It can only handle a certain type of things, but if you want to add more, you can do so. You can add more checks. You can check for read, write, execute on every single, you know, group, user group, whatever. You can do it, um check the directory. You can make basically LS again completely from scratch using what you know now. You can access the access times, the file names, obviously, file sizes, obviously. You can access the user, sh- certainly, you can access the permissions, obviously. So, you can recreate this entire output here from scratch. Also, if a stretch goal, why don't you alphabetize this stuff? Because right now, it's not alphabetized. It's based off like creation date, I think. Well, maybe not. Maybe it's alphabetic at first, but if you make new files, it's not going to be in order. And so, in that case, well, maybe put that in. I don't know. Could be cool to do that. So, yeah, either way, we have a a working LS alternative. And now, curious, how big is our our function? If I uh Uh what? Ar- Okay, sorry. It's It's big because we have the debugging tools. Let's open it up and get rid of the debugging tools. I'm like, why is it so big? What the heck? Let's clear this out. Run. And now, we're back to the 888. So, yeah, that two bytes difference, it was the error I was putting in there. So, either way, we're done. We re-implemented LS from scratch essentially here, um in X64 Linux assembly. So, yeah, pat yourself on the back. That was a pretty big task we accomplished here today in not very much time. So, hope that was interesting. If not, sorry. If it was, great. I'll see you in the next video.