{"id":19476935,"url":"https://github.com/leogaudin/libasm","last_synced_at":"2026-03-02T22:03:26.851Z","repository":{"id":249817113,"uuid":"832175720","full_name":"leogaudin/libasm","owner":"leogaudin","description":"A guide  for libasm, a project to get familiar with Assembly language.","archived":false,"fork":false,"pushed_at":"2024-08-16T09:12:49.000Z","size":53,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-11-10T19:49:43.327Z","etag":null,"topics":["42","assembly","libasm","x86-64"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leogaudin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-22T13:33:17.000Z","updated_at":"2024-10-01T08:26:38.000Z","dependencies_parsed_at":"2024-08-02T12:21:31.488Z","dependency_job_id":null,"html_url":"https://github.com/leogaudin/libasm","commit_stats":null,"previous_names":["leogaudin/libasm"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leogaudin%2Flibasm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leogaudin%2Flibasm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leogaudin%2Flibasm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leogaudin%2Flibasm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leogaudin","download_url":"https://codeload.github.com/leogaudin/libasm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232988669,"owners_count":18607449,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["42","assembly","libasm","x86-64"],"created_at":"2024-11-10T19:42:34.322Z","updated_at":"2026-03-02T22:03:21.809Z","avatar_url":"https://github.com/leogaudin.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align='center'\u003e\n\t\u003ch1\u003elibasm\u003c/h1\u003e\n\t\u003cp\u003e\u003ci\u003eAll the explanations in this file follow the System V AMD64 ABI calling convention.\u003c/i\u003e\u003c/p\u003e\n\t\u003cimg src=\"https://img.shields.io/badge/-100%2F100-success?logo=42\u0026logoColor=fff\" /\u003e\n\u003c/div\u003e\n\n## Table of Contents\n\n- [Apple Silicon](#apple-silicon) \n- [Prerequisites and basic concepts](#prerequisites-and-basic-concepts) 📚\n\t- [Registers](#registers) 📦\n\t- [Execution flow](#execution-flow) ⏩\n\t- [Syntax](#syntax) 💻\n\t\t- [Instructions](#instructions) 🤖\n\t\t- [Behavior](#behavior) 🔄\n\t\t- [Addresses and values](#addresses-and-values) 📍\n\t\t- [Exporting symbols](#exporting-symbols) 📤\n- [ft_strlen](#ft_strlen)\n- [ft_strcpy](#ft_strcpy)\n- [ft_strcmp](#ft_strcmp)\n- [ft_write](#ft_write)\n- [ft_read](#ft_read)\n- [ft_strdup](#ft_strdup)\n- [Resources](#resources) 📖\n\n## Apple Silicon\n\nApple Silicon is based on ARM64 architecture. The assembly code in this repository is written for `x86_64` architecture.\n\nIf you wish to use and test your assembly code seamlessly on those chips, you need to add the following lines to your Makefile:\n\n```makefile\nCC = gcc\nifeq ($(shell uname -m), arm64)\n\tCC += -ld_classic --target=x86_64-apple-darwin\nendif\n```\n\nInstead of rewriting all the assembly code for ARM64 architecture, we basically compile our C code in `x86_64` architecture as it can be executed through Rosetta 2.\n\n\u003e Don't forget the `-f macho64` flag after your `nasm` command when compiling on Macs.\n\n# Prerequisites and basic concepts\n\n## Registers\n\nAssembly works mainly with registers. They can be compared to little pre-defined boxes that can store data.\n\nThe general-purpose registers are:\n- `rax`\n- `rcx`\n- `rbx`\n- `rdx`\n- `rsi`\n- `rdi`\n- `rsp`\n- `rbp`\n- `r8`, `r9`, ...`r15`\n- `rip`\n\nWe have to be careful when putting data in those registers, because they can be used by the system at any time (read or written).\n\nAmong the above registers for example:\n- `rax` is used to store the return value of a function.\n- `rcx` is used as a counter in loops.\n- `rsp` is used to store the stack pointer.\n- `rbp` is used to store the base pointer.\n- `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` are used respectively to pass arguments, vulgarly like `function(rdi, rsi, rdx, rcx, r8, r9)`\n\n\u003e This may seem like an inconvenience, but it is actually very useful to manipulate the behavior of the program.\n\u003e\n\u003e For instance, if we want to call `sys_write`:\n\u003e 1. We put the syscall number (4 for `sys_write`) in `rax`.\n\u003e 2. We put the file descriptor in `rdi`.\n\u003e 3. We put the address of the buffer in `rsi`.\n\u003e 4. We put the number of bytes to write in `rdx`.\n\u003e 5. We call the `syscall` instruction.\n\nThe conclusion is: **the little boxes are very useful as intermediate storage for data, but we have to be careful not to overwrite them when we** (or the system) **need them.**\n\n## Execution flow\n\nAssembly is read **from top to bottom**.\n\nThe instructions can be grouped in **labels**, which are used to **mark a specific point in the code**. They are followed by a colon.\n\nOne thing that can be confusing is that although labels look like functions in other languages like C, they are not.\n\nFor example:\n```nasm\nentry_point:\n\txor rax, rax\n\ndo_something:\n\tmov rax, 0\n\ndo_something_else:\n\tmov rax, 1\n\nreturn_label:\n\tret\n```\n\nIn this example, `entry_point` is the entry point of the program/function.\n\n`do_something` and `do_something_else` will be executed one after the other, even without a \"jump\" instruction.\n\n## Syntax\n\n\u003e The syntax used in this repository is the Intel syntax. It is the most common syntax used in assembly programming, and a requisite of the subject. It is characterized by the fact that the destination operand is on the left and the source operand is on the right.\n\n## Instructions\n\nVirtually **all the lines in assembly are** composed of an **instruction followed by its operand(s)**.\n\nA few examples:\n\n\n- `mov rax, 0` copies the value `0` into the register `rax`.\n\n- `add rax, 1` adds `1` to the value in the register `rax`.\n\n- `cmp rax, 0` compares the value in the register `rax` with `0`.\n\n- `jmp do_something` jumps to the label `do_something`.\n\n## Behavior\n\nIt is very important to remember that every instruction can alter the behavior of the program implicitly.\n\nFor example:\n\n- The `cmp` instruction will set the flags register according to the result of the comparison.\n- The `loop` instruction will decrement the `rcx` register and jump to the label if `rcx` is not zero.\n\n## Addresses and values\n\nLike in C, we can work with addresses.\n\nThe square brackets `[]` are used to dereference an address.\n\nFor example, if we want to move the value at the address `0x1234` into the register `rax`, we can do:\n\n```nasm\nmov rax, [0x1234]\n```\n\nIf we want to compare the address 3 bytes after the address in `rax` with `0`, we can do:\n\n```nasm\ncmp [rax + 3], 0\n```\n\n\u003e Here we should technically use an identifier for the address (`BYTE`, `WORD`, `DWORD`, `QWORD`) to specify the size of the data we want to compare, but we ignored it for the sake of this explanation.\n\n## Exporting symbols\n\nIn order to use the functions we write in assembly in a C program, we need to export them.\n\nTo do so, we can use the `global` directive.\n\nFor example, if we want to export the function `ft_strlen`, we can do:\n\n```nasm\nglobal ft_strlen\n\nft_strlen:\n\t...\n```\n\n# ft_strlen\n\nThe `ft_strlen` function is a function that returns the length of a string. It is a very simple function that iterates over the string until it finds the null-terminator (`\\0`).\n\nTo implement it in assembly, we need to recapitulate the behavior of the function:\n1. Set a counter to 0.\n2. Look at the first character of the string.\n3. Increment the counter.\n4. Look at the next character.\n5. If it is not the null-terminator, increment the counter and go back to step 4.\n6. If it is the null-terminator, return the counter.\n\nTo replicate this behavior in assembly, we will need to learn a few instructions:\n- `mov` to copy data.\n- `jmp` to jump.\n- `cmp` to compare data.\n- `je` to jump if equal.\n- `inc` to increment a register.\n- `ret` to return from the function.\n\n### `mov`\n\n```nasm\nmov rax, rdi\n```\n\nThis instruction copies the value in `rdi` to `rax`.\n\n### `jmp`\n\n```nasm\nentry_point:\n\tjmp some_other_label\n\nsome_label:\n\t...\n\nsome_other_label:\n\t...\n```\n\nThe first line of `entry_point` will jump to the label `some_other_label` (and skip `some_label`) regardless of the condition.\n\nJumps work like a `goto` in C. They can be used to skip parts of the code, or to create loops (as they can jump to a label that is located earlier in the code).\n\n### `cmp`\n\n```nasm\ncmp rax, rdi\n```\n\nThis instruction compares the value in `rax` with the value in `rdi`.\n\nIf we want to check if `rax` is equal to `rdi`, we can do:\n\n```nasm\ncmp rax, rdi\nje equal\n```\n\nWhich leads us to the next instruction.\n\n### `je`\n\n```nasm\ncmp some_register, some_other_register\nje equal\n```\n\nThis instruction will jump to the label `equal` only if the two registers are equal.\n\n### `inc`\n\n```nasm\ninc rax\n```\n\nThis instruction increments the value in `rax` by `1`. Simple.\n\n### `ret`\n\n```nasm\nret\n```\n\nThis instruction returns from the program/function.\n\n\u003e Remember that in the System V AMD64 ABI, the return value of a function is stored in the `rax` register. Whatever is in `rax` when we call `ret` instruction will be the return value of the program/function.\n\n## Implementation\n\nNow that we know the instructions we need, we can implement the `ft_strlen` function.\n\nThe first thing we need to do is to **set the counter to 0**. We can do this by moving `0` to `rcx` (or any other register, but remember `rcx` is commonly used as a counter).\n\n```nasm\nmov rcx, 0\n```\n\nThen, we need to define our recurring loop.\n\nWe need to look at every character in the string passed in `rdi` (where is passed the first argument in this calling convention, as seen before).\n\nSo, **`rdi` first points at the first character of the string**, and **`rcx` is our counter initialized to 0**.\n\nLike we would look into `*(str + i)` in C, we can use assembly's square brackets `[]` as follows:\n\n```nasm\ncmp [rdi + rcx], 0\n```\n\nWhat happens next?\n- If the character is **not the null-terminator**, we need to **increment the counter and go back** to the beginning of the loop.\n- If **it is the null-terminator**, we need to **return the counter**.\n\nSo, with the instructions we learned before, we can use:\n- `cmp` to compare the character with `0`\n- `je` to jump to the end of the function if it is the null-terminator.\n- `inc` to increment the counter.\n- `jmp` to go back to the beginning of the loop.\n\nOur loop would then look like:\n\n```nasm\nloop:\n\tcmp [rdi + rcx], 0\n\tje end\n\tinc rcx\n\tjmp loop\n```\n\nFinally, we need to return the counter. We can do this by moving the value in `rcx` to `rax` and returning.\n\n\u003e Remember, that *\"whatever is in `rax` when we call `ret` instruction will be the return value of the program/function.\"*\n\n```nasm\nend:\n\tmov rax, rcx\n\tret\n```\n\nAnd that's it! We have implemented the `ft_strlen` function in assembly.\n\n# ft_strcpy\n\nThe `ft_strcpy` function is a function that copies a string into another string, and returns a pointer to the destination string.\n\nThe logic is very similar to the `ft_strlen` function:\n\n1. Set a counter to 0.\n2. Look at the first character of the source string.\n3. Copy it to the destination string.\n4. Increment the counter.\n5. Look at the next character and copy it.\n6. If it is not the null-terminator, increment the counter and go back to step 5.\n7. If it is the null-terminator, exit the loop and return a pointer to the destination string.\n\n## Implementation\n\nA pointer to our `dst` string is passed in `rdi`, and a pointer to our `src` string is passed in `rsi`.\n\nWe can start the same way we did with `ft_strlen` by setting the counter to 0.\n\n```nasm\nft_strcpy:\n\tmov rcx, 0\n```\n\nWe can then start our loop.\n\nWe need to copy every character in `rsi` to `rdi`. However, **in assembly, we can't copy data directly from one address to another** (`mov [rdi], [rsi]` would not work).\n\nWe therefore need to copy the data from the source address to a register, and then copy it to the destination address.\n\n\u003e We could use any register to store the character (like `r8` as seen before), but it is more appropriate to use `al` for this purpose, as it is a register that is meant to store a single byte.\n\n```nasm\nloop:\n\tmov al, [rsi + rcx]\n\tmov [rdi + rcx], al\n\tinc rcx\n\tcmp al, 0\n\tjne loop\n```\n\nIn this loop:\n1. We copy the character in `rsi` to `al`.\n2. We copy `al` to `rdi`.\n3. We increment the counter.\n4. We check if the character is the null-terminator.\n5. If it is not, we go back to the beginning of the loop.\n\nFinally, we need to return the pointer to the destination string.\n\nGiven that we received this pointer in `rdi` and that we did not move it, we can simply copy it to `rax` and return.\n\n```nasm\nmov rax, rdi\nret\n```\n\n# ft_strcmp\n\nThe `ft_strcmp` function is a function that compares two strings, and returns the difference between the first two different characters. For example, \"abc\" and \"abd\" would return -1, as 'c' - 'd' = -1.\n\nIts logic is **not much more complex** than the previous functions, but **the particularities of assembly make it a bit more challenging** (for me at least, maybe I have a shitty logic).\n\nLet's recap the behavior of the function anyway:\n1. Set a counter to 0.\n2. Compare every character of the first string with every character of the second string.\n3. If they are different, substract the second character from the first character and return the result.\n\nFrom this exercise on, I will only explain the new instructions and concepts we use.\n\n### `sub`\n\n```sub rax, rdi``` performs a substraction such as `rax` ← `rax` - `rdi`.\n\n### `movzx`\n\n```movzx rax, BYTE[rdi + rcx]``` moves the byte at the address `rdi + rcx` to `rax`, and fills the remaining bits with 0.\n\n\u003e movzx will adapt to the keyword used to specify the size of the data we want to move. For example, `movzx rax, WORD[rdi + rcx]` would move a word (16 bits) to `rax`.\n\nThis instruction is useful to us as `rax` is a 64-bit register, and we only want to compare the characters as bytes (8 bits).\n\n### `jz`\n\n```jz label``` jumps to `label` if the zero flag is set, a bit like `je` and `jne` but for the zero flag.\n\nFor example, if we want to jump to `end` if one of `register1` or `register2` is 0, we can do:\n\n```nasm\ncmp register1, 0\ncmp register2, 0\njz end\n```\n\n## Implementation\n\n**I will not show code like in the previous ones**, just describe the logic with more depth.\n\nMy intermediates registers will be `rax` and `r8` (not the most efficient code but easier to understand).\n\n1. We set `rcx`, `rax` and `r8` to 0.\n2. We start our loop.\n3. We copy `rdi` and `rsi` to `rax` and `r8`.\n4. We check that the characters are not the null-terminator.\n5. We compare the characters.\n6. If they are different, we substract them and return.\n7. If not, we increment the counter and go back to the beginning of the loop.\n\nThat's it! With all the precautions I mentioned and the new instructions, you should be able to implement the `ft_strcmp` function.\n\n# ft_write\n\nThe `ft_write` function is a function that, provided a file descriptor, a buffer and a size, writes the buffer to the file descriptor. It returns the number of bytes written, or -1 if an error occurred.\n\nTo implement this function, we need to know how to make a syscall.\n\n## Syscalls\n\nAs we saw in the introduction, to make a syscall, we need to:\n1. Put the syscall number in `rax`.\n2. Put its arguments in the appropriate registers (here `rdi`, `rsi` and `rdx`).\n3. Trigger the syscall with the `syscall` instruction, that will read the values in the registers and behave accordingly.\n\n## First steps\n\nIn our case, the parameters we receive from C are already in the right registers, so we can save the following instructions:\n```nasm\nmov rdi, rdi ; file descriptor\nmov rsi, rsi ; buffer\nmov rdx, rdx ; count\n```\n\nWe can then put the syscall number in `rax` and call the syscall.\n\nOn Apple Silicon Macs, the syscall number for `sys_write` is `0x2000004`. On Linux, it is `1`.\n\n```nasm\nmov rax, 0x2000004\n; mov rdi, rdi\n; mov rsi, rsi\n; mov rdx, rdx\nsyscall\nret\n```\n\nWe could stop here, as the function would properly write to the file descriptor and return the number of bytes written (the syscall putting its return value in `rax`).\n\nHowever, **we need to return `-1` if an error occurred, and set the `errno` variable accordingly**, as asked in the subject.\n\nTo handle any error and jump to another label, we can use the `jc` instruction. This will **jump to the label if the Carry Flag is set**, which is generally the case when an error occurs.\n\n```nasm\njc error\n```\n\n### `errno`\n\n`errno` is a variable that is set when an error occurs. It is a global variable that is set by the system when a syscall fails.\n\nHowever, **it is not automatically translated to the `errno` variable in C**. We need to set it ourselves.\n\nTo do so, we are provided with `__errno_location` (or `___error` on Mac). This function returns a pointer to the `errno` variable.\n\nWe can call it with the instruction `call __errno_location`. As for other instructions, it will put the return value in `rax`.\n\nHowever, **`rax` already contains the return value of the `write` syscall**. We need to **save it** before calling `__errno_location`.\n\n```nasm\nerror:\n\tmov r8, rax\n\tcall __errno_location\n```\n\nWe now have the address of the `errno` variable in `rax`. We can put the previously saved error code in it by dereferencing the address.\n\n```nasm\nmov [rax], r8\n```\n\nFinally, we can return `-1` and exit the function.\n\n```nasm\nmov rax, -1\nret\n```\n\nAnd that's it! We have implemented the `ft_write` function.\n\n# ft_read\n\nThe `ft_read` function is a function that, provided a file descriptor, a buffer and a size, reads from the file descriptor to the buffer. It returns the number of bytes read, or -1 if an error occurred.\n\nIt's literally implementing the `ft_write` function but with the `sys_read` syscall.\n\nSame logic, same instructions, same everything.\n\nSo we just replace `0x2000004` with `0x2000003` and we're good to go.\n\nEasy.\n\n# ft_strdup\n\nThe `ft_strdup` function is a function that duplicates a string. It allocates memory for the new string, copies the string to it, and returns a pointer to the new string.\n\nDoesn't sound as easy as `ft_read`.\n\nLuckily, we can use the functions we implemented before, and the `malloc` function. But we also need to learn two new instructions.\n\n### `push` and `pop`\n\nWe have another way to store data in assembly: the stack.\n\nThe stack is literally a pile of data, organized in a LIFO (Last In, First Out) way.\n\nIf I push `1`, `2` and `3` on the stack, it will look like:\n```\n3\n2\n1\n```\n\nIf I pop the stack here, I will get `3`.\n\nIn assembly, we can manipulate the stack as follows:\n\n```nasm\npush rax\npush rbx\n```\n\nThis instruction will push the value of `rax`, then the value of `rbx` on the stack.\n\n```nasm\nvalue of rbx\nvalue of rax\n```\n\nWe then have the `pop` command, that will take the last value pushed on the stack and put it in the operand we specify.\n\n```nasm\npop any_register\n```\n\nAfter this instruction, `any_register` will contain the value of `rbx`.\n\n## Implementation\n\nIn this implementation, we will use `push` and `pop` to save the values of our registers when calling other functions that manipulate them.\n\nLet's recap the steps of the function:\n1. Get the length of the string (`ft_strlen`).\n2. Allocate memory for the new string (`malloc`).\n3. Copy the string to the new string (`ft_strcpy`).\n\nWe also know that:\n- When `ft_strdup` is called, **`rdi` contains `*s`** (the string to duplicate).\n- `ft_strlen` **reads a string in `rdi`** and **returns the length in `rax`**.\n- `malloc` **reads the size in `rdi`** and **returns a pointer** to the allocated memory **in `rax`**.\n- `ft_strcpy` **reads the source string in `rsi`** and **the destination string in `rdi`**.\n\nSo:\n1. We don't need to touch `rdi` before calling `ft_strlen`.\n2. We call `ft_strlen`, that saves the length in `rax`.\n3. We push the `rdi` (that contains `*s`) to the stack, to use it later.\n4. We increment the length by 1 (for the null-terminator).\n5. We move the length of `*s` to `rdi` to call `malloc` with the right size.\n6. We call `malloc`, that returns a pointer to the allocated memory in `rax`.\n7. We pop the `*s` from the stack to `rsi` (second argument of `ft_strcpy`).\n8. We move the pointer to the allocated memory to `rdi` (first argument of `ft_strcpy`).\n9. We call `ft_strcpy`, that copies the string to the allocated memory.\n10. We can directly call `ret` as the pointer to the new string is already in `rax`.\n\nAnd that's it! We have implemented the last mandatory function of the project.\n\n## Changes to make on Linux\n\n- The syscall number for `sys_write` is `1` on Linux.\n- The syscall number for `sys_read` is `0` on Linux.\n- The `___error` function is named `__errno_location` on Linux.\n- The symbols don't need to be prefixed with an underscore on Linux.\n- The Carry Flag is not set when an error occurs on Linux. We need to check if `rax` is negative to detect an error.\n- The nasm flag `-f elf64` should be used instead of `-f macho64`.\n\n# Resources\n\n- [x64 Cheat Sheet](https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf)\n- [Le langage assembleur intel 64 bits](https://www.lacl.fr/tan/asm)\n- [___error on Mac vs __errno_location on Linux](https://github.com/cacharle/libasm_test/issues/2)\n- [Assembly File Management](https://www.tutorialspoint.com/assembly_programming/pdf/assembly_file_management.pdf)\n- [The Stack: Push and Pop](https://www.cs.uaf.edu/2015/fall/cs301/lecture/09_16_stack.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleogaudin%2Flibasm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleogaudin%2Flibasm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleogaudin%2Flibasm/lists"}