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