Source: https://moabid.me/blog/c-from-another-prespective/ Published: 2023-03-27 Domains: Systems Programming Topics: Secure Coding, Code Quality Languages: C
With this, I am starting a series of articles explaining my perspective on C: how I actually write C code. My approach was inspired by my friend enijakow, who deserves the credit for that, ngl :D. In this article, I am going to discuss the following:
Okay, okay if you did spend enough time writing C code, you would have come across many people complaining about how C is unsafe and how you should switch to Rust. Well to answer that I would step in and say that’s bullshit! C gives you the power over the machine, if you can’t write safe code that’s actually your fault not C’s fault, C is a tool and you are its master. It’s a language that allows you to configure and control the memory which is considered until this day the major strong point that C has.
However, to write safe C code, you need to follow some conventions. These conventions are not just there for the sake of aesthetics or standards, but they actually help you avoid common programming errors that can lead to security vulnerabilities or crashes. Here are some C conventions that you should follow (you better start using them after this 😒 ):
// First thing, I don't know why everyone is using structs like this :
typedef struct s_data {
int some_data;
char *some_more;
// ...
} t_data;
// Instead of using it like this :
struct s_data {
int some_data;
char *some_more;
// ...
};
// Yes you can use both the first one is simply using typedef to
// access struct s_data while only writing t_data instead of writing
// struct s_data. I mean yeah you are probably using it,
A quick explanation of what is a typedef:
Typedef is simpling creating an alias for the name. In general, an alias is a different name given to something that already has a name. In the context of coding, an alias is a way to give a shorter or more convenient name to a longer or more complex name. Here is a dummy example :
typedef int mysuperbeautifulint;
mysuperbeautifulint var;
printf("The size of int = [%d] and the size of mysuperbeautifulint = [%d]\n", \
sizeof(int), sizeof(mysuperbeatifulint);
// The size of int = [4] and the size of mysuperbeautifulint = [4]
From this example, we can see the size of the int and mysuperbeatifulint stayed the same. And now from this explanation, I think you have a better idea of what this t_data actually means. And now let's see why I don’t like this type of declaration.
Let's think of this scenario:
You have an application where you are using Struct, unions, and enums (If you don’t know what are those I am gonna leave some resources downstairs; ops I mean down below the article 🤓 ) at the same time, how are you gonna declare them? t_data, e_data, u_data ? Like this? If that is your approach good luck keeping your code readable and being able to differentiate between all of them 🙂.
#include <stdio.h>
// define a struct named "Person"
typedef struct {
char name[20];
int age;
enum e_gender { Male, Female } gender; // This is a boolean value, so
} t_person; // we don't really need an enum.
// define a union named "Number"
typedef union {
int integer;
float floating_point;
char string[20];
} u_number;
int main() {
// create a struct instance and initialize its members
t_person person1 = {"John Doe", 25, Male};
// create a union instance and assign a value to one of its members
u_number num1;
num1.integer = 10;
// print the values of the struct and union instances
printf("Name: %s, Age: %d, Gender: %d\n", person1.name, person1.age, person1.gender);
printf("Integer: %d, Floating point: %f, String: %s\n", num1.integer, num1.floating_point, num1.string);
return 0;
}
Or this :
#include <stdio.h>
// define a struct named "Person"
struct s_person {
char name[20];
int age;
enum e_gender { Male, Female } gender; // Here again, Male and Female,
}; // 1 and 0, boolean only.
// define a union named "Number"
union u_number {
int integer;
float floating_point;
char string[20];
};
int main() {
// create a struct instance and initialize its members
struct s_person person1 = {"John Doe", 25, Male};
// create a union instance and assign a value to one of its members
union u_number num1;
num1.integer = 10;
// print the values of the struct and union instances
printf("Name: %s, Age: %d, Gender: %d\n", person1.name, person1.age, person1.gender);
printf("Integer: %d, Floating point: %f, String: %s\n", num1.integer, num1.floating_point, num1.string);
return 0;
}
Don’t you think that when working on a huge and complex application, it’s gonna get messy? I like to visualize what I am doing, which part of the code does what, what this variable means, and what its purpose is. I don’t like spending time looking and trying to remember what was this var again: / Anyways, let's keep going! Now let's tackle the function naming conventions.
// My way of writing code now (or better say Eric's way of writing code)
int main()
{
struct s_data data; // You are convinced now right ? : )
// ----------------------
data_create(&data); // My code is devided into blocks and the moment
// i create smth I write immidiatly the destroy
blablacodehere(); // to keep track of my memory and not have
// +1.5M leaks in the end and spend 2 months
// checking the leaks/double free/unconditional jumps ...
data_destroy(&data); // Just try it believe me.
// ----------------------
return (0);
}
In addition of that if you read my function naming carefully you are gonna see that I build my function name like this :
data_create(&data);
<---> <----->
/*
Target_of_my_function + _ + The_action.
(like struct or the object (The action that
this function is trying to made by the function).
modify).
*/
/*
In addition of that the first parameters is always the object this
functinon is trying to change/read.
*/
Working with this methodology did spare me hours of debugging looking where are my memory leaks.
What we have to fix is our behavior and how we write code, not the language :)
// This is the main of my implementation of the readelf command
// If you are interested in the hole source code, check this :
// https://github.com/moabid42/readelf
int main(int argc, char* argv[])
{
struct elf_file elf;
if (argc > 2 || argc == 1)
usage(argv[0]);
// ------------------------------------ This is the first scope
if (elf_file_create(&elf, argv[1]))
{
// ------------------------------- This is the second scope
if (elf_file_magic_check(&elf))
{
// ----------------------------- And this is the last scope
if (elf_file_version_64(&elf))
elf_file_64_parse(&elf);
else
elf_file_32_parse(&elf);
// ----------------------------- and here it ends the last scope
}
// ------------------------------ This is the end of the third scope
// ------------------------------ This is the forth scope
elf_file_destroy(&elf);
// ------------------------------ This is the end of the forth scope
}
// ---------------------------------- This is the end of the first scope
return (0);
}
Even when using small functions like :
// -------------------> I prefer writing this
bool is_square_equal(int num, int compared_with)
{
return (num * num == compared_with);
}
int ft_sqrt(int nb)
{
int index;
index = 0;
if (nb < 0)
return (0);
while ((is_square_equal(index, nb) == false) && i <= 46340)
index++;
if (is_square_equal(index, nb) == true)
return (index);
return (0);
}
// -------------------> Instead of this
int ft_sqrt(int nb)
{
int i;
i = 0;
if (nb < 0)
return (0);
while (nb != (i * i) && i <= 46340)
i++;
if (i * i == nb)
return (i);
return (0);
}
Ofc this is a simple example, so we can’t really see the real power of using this convention, but still through experience writing this simple and easy-to-read code, will not only make it much easier for even you to read your own code, yeah even you, I know many friends that struggle to understand their actual code (Kinda looks sus but yeah you got what I mean🧐).
Note :
You can always inline those functions if you are that concerned with speed. 🙂
const int MAX_SIZE = 100; // declaring a constant variable to avoid accidently
// changing it.
int main() {
// code that uses MAX_SIZE
}
Using static can be tricky, I will let you discover how static variables work yourself, however for functions we mostly use them when we are using a function only locally and we don’t want to have any dependencies collisions (2 functions with the same name as an example in different files).
There are some details in C that beginners often miss, which can lead to errors and frustration. Here are some important details to keep in mind:
Spend your time learning about memory, believe me, it’s worth it.
Pointers are your swords that will allow you to defeat compilation errors.
Let your goal be writing code that compiles from the first try (It’s an order, just do it 😤 , just kidding but yeah it hits differently believe me.) I don’t like spending hours fighting with a segfault and you as well, right?
One of the major concepts that beginners don’t understand is that a string can be read only and read and write. You are probably right now: Hmm what the hell does that even mean? 🤬 Chill! Relax! I am gonna break it down. In C, a string is a sequence of characters (“hello” is \[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’\] )that are stored in contiguous memory locations. When you declare a string in C, you can define it as either a constant read-only string or a read-and-write string. The main difference is that the read-only string will generate a segmentation fault, if you try to change any character in this sequence because this memory has been identified as read-only, so you can only read the value there, and not edit them. On the other hand, the read-write memory is a memory that allows you to edit any character in that sequence.
char *my_name = stdup("mouad"); // Yeah that's my name
my_name[2] = 'r'
printf("My name is : %s\n", my_name);
// My name is morad
// Well you see someone changed my name now cuz my address wasn't read only :(
Another dummy example :
char *read_only_string = "1dk wh4t sh0uld 1 s4y";
char *read_and_write_string = strdup("D0n't 3d1t m3 pl34s3");
My advice here experiment with them a little bit and try to edit the char index 0 in both cases and what you are gonna receive.
We learn by doing, that how we escape the tutorial hell.
C is a powerful language, but it does have some limitations. Here are some common limitations and how to overcome them:
C is your playground you can do whatever you want with it (Use abstraction to build anything you want, sky is your limit, or maybe not :)
Again use abstruction if you think of a struct as an object, it’s an object.
If C gives you a sword, learn how to use it, don’t cut yourself with it. This much flexibility is a feature not a bug 🐛 .
Overall, C is a powerful language that gives you a lot of control over the machine, but it requires careful coding practices to avoid errors and security vulnerabilities. By following conventions, understanding important details, and overcoming limitations, you can write safe and effective C code.
This Youtube Channel is Gold: https://www.youtube.com/watch?v=4OGMB4Fhh50&list=PLBlnK6fEyqRhX6r2uhhlubuF5QextdCSM I recommend watching as much as you can, but if you are looking for a specific topic, you can search for: name of the topic + neso academy (e.g pointers neso academy). Also this: https://www.youtube.com/watch?v=443UNeGrFoM&t=1544s
If you want more resources: Google it! That’s also part of the game.
Originally published on Medium.