forked from xolatile/xhartae
117 lines
6.7 KiB
C
117 lines
6.7 KiB
C
/*
|
|
Copyright (c) 2023 : Ognjen 'xolatile' Milan Robovic
|
|
|
|
Xhartae is free software! You will redistribute it or modify it under the terms of the GNU General Public License by Free Software Foundation.
|
|
And when you do redistribute it or modify it, it will use either version 3 of the License, or (at yours truly opinion) any later version.
|
|
It is distributed in the hope that it will be useful or harmful, it really depends... But no warranty what so ever, seriously. See GNU/GPLv3.
|
|
*/
|
|
|
|
#ifndef CHAPTER_0_SOURCE
|
|
#define CHAPTER_0_SOURCE
|
|
|
|
#include "chapter_0.h"
|
|
|
|
/*
|
|
Function 'in' will perform read system call, literally reading 'size' bytes from standard input, which is terminal in most kernels unless it's redirected to some other file
|
|
descriptor, and store it at 'data' memory address, if there's enough space in it. Since this is one of the core functions, if it fails, we want to abort the program and see what
|
|
we did wrong... Maybe there wasn't enough space in 'data', maybe 'size' was negative and it overflowed because read system call internally uses 'size_t / unsigned long int', which
|
|
is 64 bits wide, maybe we made some other mistake? Just abort, find the error and fix it.
|
|
*/
|
|
|
|
void in (void * data, int size) {
|
|
fatal_failure (data == NULL, "in: Failed to read from standard input, data is null pointer.");
|
|
fatal_failure (size == 0, "in: Failed to read from standard input, size is zero.");
|
|
|
|
(void) read (STDIN_FILENO, data, (unsigned long int) size); // I cast to void type return value of read and write, because I don't really care about it.
|
|
}
|
|
|
|
/*
|
|
Similar to 'in' function and read system call, write system call will store 'size' bytes from 'data' memory address into standard output, which is usually what you see in your
|
|
terminal. Now, I won't talk much about teletypes, virtual terminals, file descriptor redirections and similar stuff, because I'm not very knowledgable about them. What matters is,
|
|
you have a working operating system, terminal and compiler, and you can make things happen. Once you learn C better than me, and start writing your own multi-threaded kernel, core
|
|
libraries, compilers and what not, you'll care about those things.
|
|
*/
|
|
|
|
void out (void * data, int size) {
|
|
fatal_failure (data == NULL, "out: Failed to write to standard output, data is null pointer."); // Notice how we can use function 'fatal_failure' before its' definition.
|
|
fatal_failure (size == 0, "out: Failed to write to standard output, size is zero."); // That's because we declared it in 'chapter_0.h' header file.
|
|
|
|
(void) write (STDOUT_FILENO, data, (unsigned long int) size);
|
|
}
|
|
|
|
/*
|
|
Function 'echo' is just a simplification of function 'out', because we'll be using it a lot, notice that it must accept null terminated strings, which are sort of C-style thing. I
|
|
really like them, because you don't need to always know size of the string in order to iterate it, but it requires some mental overhead in order to use them without creating hard
|
|
to find bugs, which is why newer programming languages consider them unsafe. They're not unsafe, they need to be used like intended.
|
|
*/
|
|
|
|
void echo (char * string) {
|
|
out (string, string_length (string)); // This function fails when we pass a string that's not null terminated, and we don't care to check for errors...
|
|
}
|
|
|
|
void fatal_failure (int condition, char * message) { // We use this function to abort the program if condition is met and to print the message.
|
|
if (condition != 0) { // If the variable 'condition' is not equal to 0, we execute the code in curly braces.
|
|
echo ("[\033[1;31mExiting\033[0m] "); // Simply printing the message using our 'echo' function, but we also use some colours, more on that later.
|
|
echo (message);
|
|
echo ("\n");
|
|
exit (EXIT_FAILURE); // This is the function (and '_exit' system call) that aborts the program with a return code.
|
|
} // If condition isn't met, function will just return, and nothing is printed, execution continues.
|
|
}
|
|
|
|
void limit (int * value, int minimum, int maximum) { // This is somewhat similar to limiting a variable to some values, inclusively, we'll use it later.
|
|
if (value == NULL) { return; } // We shouldn't dereference null pointer, but also don't want to abort the program for small mistake.
|
|
if (* value <= minimum) { * value = minimum; } // You can also align similar consecutive statements like this, we'll see it more often in switch statement later on...
|
|
if (* value >= maximum) { * value = maximum; }
|
|
}
|
|
|
|
/*
|
|
Memory management is a whole new topic that's too complex to cover it now in details, and it's the source of most security vunrabilities and hidden bugs. For now, just remember
|
|
that every program can read, write or execute only parts of the memory that the kernel allows it. Program can request new memory or release old memory, so some other programs can
|
|
use it. We'll learn to use program called Valgrind to find and fix memory related bugs in later chapters. We rely on functions 'calloc', 'realloc' and 'free' from <stdlib.h>
|
|
header file, and we'll avoid 'malloc', and use 'realloc' carefully, because they leave new memory uninitialized.
|
|
|
|
We're internally using function 'calloc' to request new memory, function 'realloc' to enlarge existing memory and function 'free' to release old memory, data that won't be used
|
|
later in the program. It's important to "free" all "malloc/calloc/realloc"-ed memory when program finishes successfully, and in some special cases, even when program fails and
|
|
aborts. It's important for safety to do so, think of it like open and close braces, if you have some allocations, you should deallocate them later.
|
|
|
|
Some examples of using them directly (not wrapping them like I like to do) are:
|
|
*/
|
|
|
|
/**
|
|
char * data = NULL;
|
|
|
|
data = malloc (20 * sizeof (* data)); // Allocates 20 bytes of memory for 'data'.
|
|
data = calloc (20, sizeof (* data)); // Allocates 20 bytes also, but initializes them to 0 value.
|
|
data = realloc (data, 20 * sizeof (* data)); // When 'data' is null pointer, it will be same as 'malloc', else it will reallocate more memory (for correct usage).
|
|
|
|
free (data); // Deallocates memory, we'll talk about "double free" later.
|
|
**/
|
|
|
|
void * allocate (int size) {
|
|
char * data = NULL;
|
|
|
|
data = calloc ((unsigned long int) size, sizeof (* data));
|
|
|
|
fatal_failure (data == NULL, "standard : allocate : Failed to allocate memory, internal function 'calloc' returned null pointer.");
|
|
|
|
return ((void *) data);
|
|
}
|
|
|
|
void * reallocate (void * data, int size) {
|
|
data = realloc (data, (unsigned long int) size);
|
|
|
|
fatal_failure (data == NULL, "standard : reallocate: Failed to reallocate memory, internal function 'realloc' returned null pointer.");
|
|
|
|
return (data);
|
|
}
|
|
|
|
void * deallocate (void * data) {
|
|
if (data != NULL) {
|
|
free (data);
|
|
}
|
|
|
|
return (NULL);
|
|
}
|
|
|
|
#endif
|