docs: added wiki markdown files to codebase
This commit is contained in:
parent
9b3b76f33d
commit
bbf798c644
|
@ -0,0 +1,41 @@
|
|||
# Build instructions
|
||||
**DISCLAIMER: To build this project you need a linux system (WSL could work for windows users, but I haven't tested it)**
|
||||
|
||||
|
||||
First you need to clone the repository:
|
||||
|
||||
`git clone https://codeberg.org/antifallobst/noxos/`
|
||||
|
||||
then there are a few tools required, that need to be installed:
|
||||
* **gcc** - compiler
|
||||
* **ld** - linker
|
||||
* **nasm** - assembler
|
||||
* **cmake** - build system
|
||||
* **xorriso** - ISO creation tools
|
||||
* **qemu** - emulator
|
||||
|
||||
There is a shell script to setup the workspace and compile.
|
||||
Just run it using the following commands:
|
||||
```bash
|
||||
cd noxos
|
||||
./build.sh
|
||||
```
|
||||
If the Bootloader (Limine ([github](https://github.com/limine-bootloader/limine))) isn't downloaded yet, the script will download it.
|
||||
|
||||
The final ISO file will be written to the *build* directory.
|
||||
|
||||
To launch the ISO, run the `run.sh` script.
|
||||
|
||||
# Debugging
|
||||
## Logs
|
||||
When running through the script, qemu is configured to write the kernels logs to *STDOUT* and a file called *noxos.log*.
|
||||
## GDB
|
||||
You can connect GDB to qemu.
|
||||
To achieve this, run `./run.sh debug`.
|
||||
QEMU will wait until you connect GDB to it as a 'remote' target on `localhost:1234`.
|
||||
```bash
|
||||
gdb build/cmake/kernel/kernel
|
||||
target remote localhost:1234
|
||||
c
|
||||
```
|
||||
(note, that the last to commands have to be 'executed' in gdb not in your shell)
|
|
@ -0,0 +1,61 @@
|
|||
# Code Style
|
||||
## Naming conventions
|
||||
- **structs** are suffixed with **_T**
|
||||
- **typedefs** are suffixed with **_t**
|
||||
- **enums** are suffixed with **_E**
|
||||
- **global** variables are prefixed with **g_**
|
||||
|
||||
Everything is **snake_case**.
|
||||
|
||||
## Code readability
|
||||
To make the code as readable and self explaining as possible, there are a few patterns that should be used.
|
||||
|
||||
### Avoid abbreviations
|
||||
Give your variables, functions, etc. meaningfull names.
|
||||
The times of 80char wide screens are over, feel free to use that space :D
|
||||
|
||||
Example: `sym_at_idx` -> `get_symbol_at_index`
|
||||
|
||||
But you're not forced to (anything) write out everything.
|
||||
For example the **Interrupt Descriptor Table** is simply referred to as **idt** (this abbreviation is btw standard).
|
||||
|
||||
### Avoid indentation nesting
|
||||
In a perfect codebase, the maximum indention level would be 3.
|
||||
The goal of NoxOS is not to be a perfect codebase, but please avoid huge indention nesting.
|
||||
To achieve this, you can break parts into extra functions and/or use the early return pattern.
|
||||
|
||||
Example:
|
||||
```c
|
||||
bool likes_pigs (void* a) {
|
||||
if (a != NULL) {
|
||||
if (a == 0xACAB) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
->
|
||||
```c
|
||||
bool likes_pigs (void* a) {
|
||||
if (a != NULL) { return true; }
|
||||
if (a != 0xACAB) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### align declarations
|
||||
|
||||
Please align declarations, definitions, etc like in the example:
|
||||
```c
|
||||
char am_i_a_variable = '?';
|
||||
uint64_t number = 0;
|
||||
bool i_waste_bits = true;
|
||||
```
|
||||
|
||||
### namespaces
|
||||
Unlike C++ C has no namespaces.
|
||||
To achieve the goal of namespaces, please prefix your functions, structs, etc. with the "namespace" they are in.
|
||||
|
||||
Example: `out_byte` -> `io_out_byte`
|
|
@ -0,0 +1,38 @@
|
|||
Welcome :D
|
||||
|
||||
Nice, that you're interested in contributing.
|
||||
|
||||
# Before Contribution
|
||||
Please read the [Code-Style wiki entry](https://codeberg.org/antifallobst/noxos/wiki/Code-Style) before you start to contribute.
|
||||
|
||||
For workspace setup and build instructions look at [this wiki entry](https://codeberg.org/antifallobst/noxos/wiki/Build-instructions).
|
||||
|
||||
**We don't accept any Assholes! If you're a fascist, racist, or discriminate others for anything, go fuck yourself.**
|
||||
|
||||
# Contributing
|
||||
|
||||
When you contribute code, please [document](https://codeberg.org/antifallobst/noxos/wiki/Kernel-documentation) it.
|
||||
You don't have to (and you also shouldn't) write romans, but a few lines for every in a header exposed function, struct etc. help others (and yourself) understanding how to interact with your code.
|
||||
|
||||
|
||||
## What dafuq is ...?
|
||||
OS development can be quite complicated/confusing. That's Ok.
|
||||
Stuff like Paging can be really weird, if you don't have osdev experience.
|
||||
But there are learning resources and places, where you can ask questions.
|
||||
|
||||
**A small overview about such resources and places:**
|
||||
- The [osdev wiki](https://wiki.osdev.org) is like wikipedia - just for hobby os developers.
|
||||
- The [osdev subreddit](https://reddit.com/r/osdev) is a great place to ask questions and share knowledge
|
||||
- Intel's [developer manual](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) is a more advanced but kinda precise information source
|
||||
|
||||
Also feel free to join the [NoxOS Matrix space](https://matrix.to/#/#noxos:systemausfall.org),
|
||||
where you can chat with other NoxOS devs and ask questions.
|
||||
**But Stay Respectful, Or We `sudo rm -rf /` Your PC ;)**
|
||||
|
||||
# Where to start?
|
||||
|
||||
Get the codebase and play a bit around with it.
|
||||
Try to understand basic concepts of the codebase, the [documentation](https://codeberg.org/antifallobst/noxos/wiki/Kernel-documentation) will help you.
|
||||
After that you can search the [Issues](https://codeberg.org/antifallobst/noxos/issues) for the `Good First Issue` label,
|
||||
to find easy improvements, etc. that you can try to implement.
|
||||
Another option is to ask in the [Matrix Space](https://matrix.to/#/#noxos:systemausfall.org).
|
|
@ -0,0 +1,9 @@
|
|||
**DISCLAIMER: We don't accept any Assholes! If you're a fascist, racist, or discriminate others for anything, go fuck yourself.** If not, welcome :)
|
||||
|
||||
Wiki Content:
|
||||
|
||||
* [Contribution guide](https://codeberg.org/antifallobst/noxos/wiki/Contribution-guide)
|
||||
* [Build instructions](https://codeberg.org/antifallobst/noxos/wiki/Build-instructions)
|
||||
* [Code Style](https://codeberg.org/antifallobst/noxos/wiki/Code-Style)
|
||||
* [Kernel Documentation](https://codeberg.org/antifallobst/noxos/wiki/Kernel-documentation)
|
||||
* [Project Roadmap](https://codeberg.org/antifallobst/noxos/wiki/Roadmap)
|
|
@ -0,0 +1,377 @@
|
|||
# Kernel Documentation
|
||||
The kernel is booted using the limine boot protocol.
|
||||
|
||||
## Directory structure
|
||||
- **boot** - all stuff related to booting / jumping into the kernel
|
||||
- **drivers** - everything from the graphics driver, to the FS drivers
|
||||
- **mm** - memory management stuff like page frames and page maps
|
||||
- **platform** - universal API to the platform specific code in the subdirs
|
||||
- **proc** - all the process/thread related stuff like the scheduler
|
||||
- **utils** - utilities like type definitions, math functions, high-level memory management
|
||||
|
||||
---
|
||||
|
||||
# General concepts
|
||||
## kernel initialization
|
||||
The single parts of the kernel are initialized in the following order:
|
||||
- **Global Descriptor Table**
|
||||
- **Page Frame Manager**
|
||||
- **Interrupt Descriptor Table**
|
||||
|
||||
## interrupt handling
|
||||
OSDev Wiki: [Interrupts](https://wiki.osdev.org/Interrupts)
|
||||
|
||||
Unfortunatly the x86 architecture doesn't provide a method to get the ID of the current interrupt.
|
||||
To solve this problem, there is a simple assembly function for every interrupt used by NoxOS.
|
||||
This function pushes its ID on the stack.
|
||||
After that it calls a common Interrupt handler, this handler will generate the current `cpu_state_T` and call the C interrupt handler implementation.
|
||||
The C implementation returns a `cpu_state_T` that will then be loaded.
|
||||
|
||||
## paging
|
||||
OSDev Wiki: [Paging](https://wiki.osdev.org/Paging)
|
||||
|
||||
There is a difference between `Virtual Memory Spaces` and the `Physical Memory Space`.
|
||||
The Physical memory space is how the data lies directly in the RAM.
|
||||
|
||||
Virtual memory spaces are a bit more tricky.
|
||||
To understand them, we have to understand first, that the physical memory space is divided into so-called **pages** / **page frames**.
|
||||
These pages have a size of 4KB.
|
||||
|
||||
A virtual memory space is a table of page mappings.
|
||||
Per default there are no pages mapped to such a table.
|
||||
When the OS maps a page to a **page table**, it says:
|
||||
"This page is now accessible from this virtual space, at this address".
|
||||
When the Computer is in paging mode, only mapped pages are accessible.
|
||||
Now every Process gets its own page table and tada: we have successfully isolated the processes from each other,
|
||||
because every process can only access the data that it needs to access.
|
||||
|
||||
---
|
||||
|
||||
**DISCLAIMER:** Only the headers are documented, because documenting the whole code itself would be very time intensive and the headers as 'public' API are the most important to document.
|
||||
|
||||
## boot
|
||||
|
||||
### boot_info.h
|
||||
The goal of this file is to provide a universal struct of information needed by the kernel at start time.
|
||||
At the moment this information is very limine specific, but the goal is to make it easy to add support for other boot protocols.
|
||||
|
||||
#### `boot_info_T` - struct
|
||||
- **framebuffer** - struct with information about the graphics buffer
|
||||
- **terminal** - bootloader terminal / log
|
||||
- **memory_map** - information about the memory layout / regions
|
||||
- **rsdp** - pointer to RSDP
|
||||
|
||||
### limine.h
|
||||
This header provides the API to "communicate" with the limine bootloader.
|
||||
More information can be found on the limine project's [GitHub](https://github.com/limine-bootloader/limine/blob/trunk/PROTOCOL.md).
|
||||
|
||||
|
||||
## mm
|
||||
|
||||
### page_frame.h
|
||||
This header provides the functions for basic interactions with pages (in the physical memory space).
|
||||
|
||||
#### `pframe_manager_init()` - function (void)
|
||||
Initializes the page frame manager, needs to be called once at kernel init.
|
||||
|
||||
#### `pframe_reserve(address)` - function (void) [Thread Safe]
|
||||
Blocks a page, so it can't be requested or anything else.
|
||||
If the page is already blocked by anything else, e.g. by a request, it won't be reserved.
|
||||
|
||||
#### `pframe_reserve_multi(address, n)` - function (void) [Thread Safe]
|
||||
Reserves the page at the given address, plus *n* pages after that page.
|
||||
|
||||
#### `pframe_unreserve(address)` - function (void) [Thread Safe]
|
||||
Unreserves a reserved page and makes it accessible again.
|
||||
|
||||
#### `pframe_unreserve_multi(address, n)` - function (void) [Thread Safe]
|
||||
Unreserves the page at the given address, plus *n* pages after that page.
|
||||
|
||||
#### `pframe_request()` - function (void*) [Thread Safe]
|
||||
Returns the physical address of a page.
|
||||
This is kind of the low level version of malloc.
|
||||
|
||||
#### `pframe_free(address)` - function (void) [Thread Safe]
|
||||
Needs a valid page address produced by `pframe_request()` as argument.
|
||||
Invalidates the address and frees it, so it can be requested again.
|
||||
This is kind of the low level version of free.
|
||||
|
||||
#### `pframe_free_multi(address, n)` - function (void) [Thread Safe]
|
||||
Frees the page at the given address, plus *n* pages after that page.
|
||||
|
||||
## platform
|
||||
|
||||
### cpu.h
|
||||
This header contains stuff directly related to the CPU.
|
||||
|
||||
OSDev Wiki: [x86 CPU Registers](https://wiki.osdev.org/CPU_Registers_x86)
|
||||
|
||||
#### `cpu_state_T` - struct
|
||||
- **cr3** - Control register 3, holds the current page table
|
||||
- **rax** - General purpose register
|
||||
- **rbx** - General purpose register
|
||||
- **rcx** - General purpose register
|
||||
- **rdx** - General purpose register
|
||||
- **rsi** - General purpose register
|
||||
- **rdi** - General purpose register
|
||||
- **rbp** - The Bottom of the current stack frame
|
||||
- **interrupt_id** - The ID of the interrupt, that captured the cpu state
|
||||
- **error_code** - Some exceptions such as the Page fault push more detailed information into here
|
||||
- **rip** - The current instruction address
|
||||
- **crs** - Segment selector of the associated IDT descriptor
|
||||
- **flags** - The CPU's FLAGS register, a status bitmap
|
||||
- **rsp** - The Top of the current stack frame
|
||||
- **ss** - Not totally sure, what this does, but it has to do with security rings
|
||||
|
||||
This struct defines a *complete* CPU state, that can be saved and restored.
|
||||
It is saved when the CPU fires an interrupt and restored by the interrupt handler when it's finished.
|
||||
This allows multithreading and exception analysis.
|
||||
|
||||
#### `cpu_flags_E` - enum
|
||||
- **CPU_FLAG_CARRY**
|
||||
- **CPU_FLAG_PARITY**
|
||||
- **CPU_FLAG_AUXILIARY**
|
||||
- **CPU_FLAG_ZERO**
|
||||
- **CPU_FLAG_SIGN**
|
||||
- **CPU_FLAG_TRAP**
|
||||
- **CPU_FLAG_INTERRUPT_ENABLE**
|
||||
- **CPU_FLAG_DIRECTION**
|
||||
- **CPU_FLAG_OVERFLOW**
|
||||
- **CPU_FLAG_IO_PRIVILEGE_0**
|
||||
- **CPU_FLAG_IO_PRIVILEGE_1**
|
||||
- **CPU_FLAG_NESTED_TASK**
|
||||
- **CPU_FLAG_RESUME**
|
||||
- **CPU_FLAG_VIRTUAL_8086**
|
||||
- **CPU_FLAG_ALIGNMENT_CHECK**
|
||||
- **CPU_FLAG_VIRTUAL_INTERRUPT**
|
||||
- **CPU_FLAG_VIRTUAL_INTERRUPT_PENDING**
|
||||
- **CPU_FLAG_CPUID**
|
||||
|
||||
### exceptions.h
|
||||
OSDev Wiki: [Exceptions](https://wiki.osdev.org/Exceptions)
|
||||
|
||||
#### `exception_type_E` - enum
|
||||
These are just the definitions of the CPU-exception interrupt IDs.
|
||||
|
||||
#### `g_exception_type_strings` - global variable
|
||||
This array of strings defines the names of the Exceptions.
|
||||
|
||||
#### `exception_handle(cpu_state)` - function (cpu_state_T*)
|
||||
If an interrupt is an exception, the interrupt handler will call this function to handle the exception.
|
||||
At the moment it will just panic, but in far future this could get expanded for page swapping, etc.
|
||||
|
||||
### gdt.h
|
||||
OSDev Wiki: [Global Descriptor Table](https://wiki.osdev.org/GDT)
|
||||
|
||||
#### `gdt_selector_E` - enum
|
||||
- **Null**
|
||||
- **Kernel Code**
|
||||
- **Kernel Data**
|
||||
- **User Null**
|
||||
- **User Code**
|
||||
- **User Data**
|
||||
|
||||
#### `gdt_descriptor_T` - struct [packed]
|
||||
**Well documented on the osdev wiki.**
|
||||
|
||||
#### `gdt_entry_T` - struct [packed]
|
||||
**Well documented on the osdev wiki.**
|
||||
|
||||
#### `gdt_T` - struct [packed / page aligned]
|
||||
A template that holds a `gdt_entry_T` for every selector
|
||||
|
||||
#### `g_default_gdt` - global variable
|
||||
The default GDT configuration loaded when the GDT gets initialized.
|
||||
|
||||
#### `gdt_init()` - function (void)
|
||||
Loads a descriptor of `g_default_gdt` as the system GDT.
|
||||
|
||||
### interrupts.h
|
||||
This header contains all the stuff, needed to init and handle Interrupts.
|
||||
|
||||
#### `idt_register_T` - struct [packed]
|
||||
This struct is very similar to the GDT descriptor.
|
||||
It holds the size and address of the Table, where the interrupt handlers are looked up.
|
||||
|
||||
#### `idt_descriptor_entry_T` - struct
|
||||
This struct stores information about one interrupt handler.
|
||||
The osdev wiki explains this more detailed.
|
||||
|
||||
#### `g_idt_register` - global variable
|
||||
The default IDT configuration loaded when the IDT gets initialized.
|
||||
|
||||
#### `idt_init()` - function (void)
|
||||
This function fills all the interrupt gates (handlers) into the IDT and loads it.
|
||||
|
||||
|
||||
## utils
|
||||
|
||||
### bitmap.h
|
||||
Provides functionalities to create, destruct and work with bitmaps.
|
||||
|
||||
#### `bitmap_T` - struct
|
||||
This struct holds a buffer for a bitmap and its size.
|
||||
The size is the size of the buffer in bytes, to get the amount of storable bits multiply size with 8.
|
||||
|
||||
#### `bitmap_init_from_buffer(buffer, size)` - function (bitmap_T)
|
||||
Creates a bitmap object from a given buffer and size
|
||||
|
||||
#### `bitmap_init(size)` - function (bitmap_T) [NOT IMPLEMENTED YET]
|
||||
Allocates memory to hold a bitmap in the given size and returns a `bitmap_T` with that buffer and size.
|
||||
|
||||
#### `bitmap_destruct(bitmap*)` - function (void) [NOT IMPLEMENTED YET]
|
||||
Frees the memory of the given bitmap created with `bitmap_init`.
|
||||
|
||||
#### `bitmap_set(bitmap*, index, value)` - function (bool)
|
||||
Sets the bit at the given index in the given bitmap to the given boolean value.
|
||||
Returns **false**, if the index is out of the bitmaps size bounds.
|
||||
Returns **true**, if the operation was successful.
|
||||
|
||||
#### `bitmap_get(bitmap*, index)` - function (bool)
|
||||
Returns the boolean value stored at the given index in the given bitmap.
|
||||
Always returns **false**, if the index is out of the bitmaps size bounds.
|
||||
|
||||
### core.h
|
||||
All the utils, which I didn't know how to name.
|
||||
|
||||
#### `CORE_HALT_WHILE(a)` - macro
|
||||
This halts until **_a_** is true.
|
||||
Used when working with blocking variables in e.g. thread safe functions.
|
||||
|
||||
#### `CORE_HALT_FOREVER` - macro
|
||||
This halts forever and warns about this in the log.
|
||||
|
||||
### io.h
|
||||
Provides basic Input/Output functionalities.
|
||||
|
||||
#### `io_out_byte(port, data)` - function (void)
|
||||
Writes one byte of **_data_** to **_port_**.
|
||||
This is a wrapper around the assembly `outb` instruction.
|
||||
|
||||
#### `io_in_byte(port)` - function (uint8_t)
|
||||
Reads one byte from **_port_** and returns it.
|
||||
This is a wrapper around the assembly `inb` instruction.
|
||||
|
||||
### logger.h
|
||||
Functionalities to write logs to QEMU's serial port.
|
||||
|
||||
#### `log_level_E` - enum
|
||||
- **Info** - General information, that could be useful
|
||||
- **Debug** - Should only be used to find bugs and removed (or commented out) after the bug is found
|
||||
- **Warning** - Used for warnings and not important errors
|
||||
- **Error** - Used for Fatal Errors / Will be printed to the screen (graphics driver is not Implemented yet)
|
||||
|
||||
#### `log(log_level, string)` - function (void)
|
||||
Logs the given string to QEMU's log port, the string is prefixed with the log type.
|
||||
|
||||
### math.h
|
||||
Mathematical functions, definitions, etc.
|
||||
|
||||
#### `MAX(a, b)` - macro
|
||||
Returns the bigger one of the given values.
|
||||
|
||||
#### `MIN(a, b)` - macro
|
||||
Returns the smaller one of the given values.
|
||||
|
||||
#### `CEIL_TO(a, b)` - macro
|
||||
Aligns **_a_** upwards to **_b_**.
|
||||
Example: `CEIL_TO(13, 8)` would return 16, because 16 is the next higher multiple of 8 after 13.
|
||||
|
||||
#### `FLOOR_TO(a, b)` - macro
|
||||
Aligns **_a_** downwards to **_b_**.
|
||||
Example: `FLOOR_TO(13, 8)` would return 8, because 8 is the next smaller multiple of 8 before 13.
|
||||
|
||||
#### `pow(base, exponent)` - function (uint64_t)
|
||||
Returns the power of `base ^ exponent`.
|
||||
|
||||
### memory.h
|
||||
Basic memory functionalities.
|
||||
|
||||
#### `memory_copy(source, destination, num)` - function (void)
|
||||
Copies **_num_** bytes from **_source_** to **_destination_**.
|
||||
On linux this function is called _memcpy_.
|
||||
|
||||
#### `memory_set(destination, data, num)` - function (void)
|
||||
Sets **_num_** bytes at **_destination_** to **_data_**.
|
||||
On linux this function is called _memset_.
|
||||
|
||||
#### `memory_compare(a, b, num)` - function (bool)
|
||||
Compares the first **_num_** bytes at **_a_** and **_b_**.
|
||||
Returns **false** if there is a different byte.
|
||||
Returns **true** if the data is the same.
|
||||
There is a similar function on linux called _memcmp_.
|
||||
|
||||
### panic.h
|
||||
Ahhhhh - the kernel is burning!
|
||||
|
||||
#### `panic(state, message)` - function (void)
|
||||
This prints out the error message, a stack backtrace (planned) and a register dump (planned).
|
||||
After that, the kernel halts forever.
|
||||
This function is called, when a fatal error occurs
|
||||
|
||||
### stdtypes.h
|
||||
Standard type definitions, that are used almost everywhere.
|
||||
|
||||
#### `uint8_t` - typedef
|
||||
8-bit wide unsigned int.
|
||||
|
||||
Range: `0` - `255`
|
||||
|
||||
#### `int8_t` - typedef
|
||||
8-bit wide signed int.
|
||||
|
||||
Range: `-128` - `127`
|
||||
|
||||
#### `uint16_t` - typedef
|
||||
16-bit wide unsigned int.
|
||||
|
||||
Range: `0` - `65536`
|
||||
|
||||
#### `int16_t` - typedef
|
||||
16-bit wide signed int.
|
||||
|
||||
Range: `-32768` - `32767`
|
||||
|
||||
#### `uint32_t` - typedef
|
||||
32-bit wide unsigned int.
|
||||
|
||||
Range: `0` - `4294967296`
|
||||
|
||||
#### `int32_t` - typedef
|
||||
32-bit wide signed int.
|
||||
|
||||
Range: `-2147483648` - `2147483647`
|
||||
|
||||
#### `uint64_t` - typedef
|
||||
64-bit wide unsigned int.
|
||||
|
||||
Range: `0` - `18446744073709551616`
|
||||
|
||||
#### `int64_t` - typedef
|
||||
64-bit wide unsigned int.
|
||||
|
||||
Range: `-9223372036854775808` - `9223372036854775807`
|
||||
|
||||
#### `bool` - typedef
|
||||
Boolean type, can hold a logical value **true** or **false**.
|
||||
|
||||
#### `true` - macro
|
||||
Logical **true** value.
|
||||
|
||||
#### `false` - macro
|
||||
Logical **false** value
|
||||
|
||||
#### `NULL` - macro
|
||||
A pointer to nowhere.
|
||||
|
||||
### string.h
|
||||
|
||||
#### `string_t` - typedef
|
||||
A null-terminated array of chars.
|
||||
|
||||
#### `string_length(string)` - function (uint32_t)
|
||||
Returns the amount of chars a string has before it's null-terminator.
|
||||
|
||||
#### `string_compare(a, b)` - function (bool)
|
||||
Returns **true** when the strings **_a_** and **_b_** are equal.
|
||||
Returns **false** if they aren't equal.
|
Loading…
Reference in New Issue