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.