Merge into main the new state of the operating system/kernel #1
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,3 +1,4 @@
|
|||||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
*.svg filter=lfs diff=lfs merge=lfs -text
|
*.svg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
demodisk.img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
build
|
build
|
||||||
|
CON
|
||||||
|
.vscode
|
||||||
|
16
.vscode/c_cpp_properties.json
vendored
16
.vscode/c_cpp_properties.json
vendored
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Linux",
|
|
||||||
"includePath": [
|
|
||||||
"${workspaceFolder}/**"
|
|
||||||
],
|
|
||||||
"defines": [],
|
|
||||||
"compilerPath": "/usr/bin/gcc",
|
|
||||||
"cStandard": "gnu17",
|
|
||||||
"cppStandard": "gnu++14",
|
|
||||||
"intelliSenseMode": "linux-gcc-x64"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 4
|
|
||||||
}
|
|
2
Makefile
2
Makefile
@ -24,7 +24,7 @@ all: clean build
|
|||||||
build: build_kernel run
|
build: build_kernel run
|
||||||
|
|
||||||
run:
|
run:
|
||||||
$(EMULATOR) -kernel $(BUILD_DIR)/myos.bin -serial stdio -vga std
|
$(EMULATOR) -kernel $(BUILD_DIR)/myos.bin -serial file:CON -vga std -monitor stdio -display gtk -m 2G -cpu core2duo -drive file=demodisk.img
|
||||||
|
|
||||||
build_kernel: $(OBJ_LINK_LIST)
|
build_kernel: $(OBJ_LINK_LIST)
|
||||||
|
|
||||||
|
@ -10,18 +10,17 @@ ________________________
|
|||||||
The first scrolling boot screen. 😲
|
The first scrolling boot screen. 😲
|
||||||
|
|
||||||
|
|
||||||
<img src="screenshots/WIP_interruptHandling.png"></img>
|
![Interrupt handeling](screenshots/WIP_interruptHandling.png) \
|
||||||
W.I.P - Working on interrupt handling
|
W.I.P - Working on interrupt handling
|
||||||
|
|
||||||
|
|
||||||
|
![Multiboot integration](screenshots/multiboot.png) \
|
||||||
|
Multiboot information can be read by the kernel.
|
||||||
________________________
|
________________________
|
||||||
|
|
||||||
### The goal
|
### The goal
|
||||||
Writing a hobby operating system to better understand the basic building blocks of any operating system.
|
Writing a hobby operating system to better understand the basic building blocks of any operating system.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
________________________
|
________________________
|
||||||
### Operating System Technical specs/details
|
### Operating System Technical specs/details
|
||||||
The operating system can print strings to the
|
The operating system can print strings to the
|
||||||
|
BIN
screenshots/multiboot.png
(Stored with Git LFS)
Normal file
BIN
screenshots/multiboot.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -23,6 +23,9 @@ void irs_handler (registers regs) {
|
|||||||
printf(" Error code: %d \n", regs.err_code);
|
printf(" Error code: %d \n", regs.err_code);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,48 @@ void kterm_putat (char c, uint8_t color, size_t x, size_t y ) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void enable_cursor (uint8_t start_cursor , uint8_t end_cursor ){
|
||||||
|
outb(0x3D4, 0x0A);
|
||||||
|
outb(0x3D5, (inb(0x3D5) & 0xC0) | start_cursor);
|
||||||
|
|
||||||
|
outb(0x3D4, 0x0B);
|
||||||
|
outb(0x3D5, (inb(0x3D5) & 0xE0) | end_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_cursor()
|
||||||
|
{
|
||||||
|
outb(0x3D4, 0x0A);
|
||||||
|
outb(0x3D5, 0x20);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void update_cursor(int x, int y){
|
||||||
|
uint16_t pos = y * VGA_WIDTH + x;
|
||||||
|
|
||||||
|
outb(0x3D4, 0x0F);
|
||||||
|
outb(0x3D5, (uint8_t) (pos & 0xFF));
|
||||||
|
outb(0x3D4, 0x0E);
|
||||||
|
outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t get_cursor_position(){
|
||||||
|
uint16_t pos = 0;
|
||||||
|
outb(0x3D4, 0x0F);
|
||||||
|
pos |= inb(0x3D5);
|
||||||
|
outb(0x3D4, 0x0E);
|
||||||
|
pos |= ((uint16_t) inb(0x3D5)) << 8;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_cursor_x (uint16_t cursor_pos) {
|
||||||
|
return cursor_pos % VGA_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_cursor_y (uint16_t cursor_pos ) {
|
||||||
|
return cursor_pos / VGA_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With the help from:
|
* With the help from:
|
||||||
@ -63,6 +105,7 @@ void kterm_scrollup(){
|
|||||||
|
|
||||||
void kterm_put (char c) {
|
void kterm_put (char c) {
|
||||||
if(++kterm_column == VGA_WIDTH || c == '\n' ) {
|
if(++kterm_column == VGA_WIDTH || c == '\n' ) {
|
||||||
|
update_cursor(kterm_column , kterm_row);
|
||||||
kterm_column = 0;
|
kterm_column = 0;
|
||||||
if(kterm_row == VGA_HEIGHT-1 ) {
|
if(kterm_row == VGA_HEIGHT-1 ) {
|
||||||
kterm_scrollup();
|
kterm_scrollup();
|
||||||
|
@ -5,12 +5,14 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#include "../vga/colors.h"
|
#include "../vga/colors.h"
|
||||||
|
#include "../../../io.h"
|
||||||
void kterm_init();
|
void kterm_init();
|
||||||
|
|
||||||
|
/* Kernel terminal - Colour functions*/
|
||||||
void kterm_resetcolor();
|
void kterm_resetcolor();
|
||||||
void kterm_setcolor(uint8_t);
|
void kterm_setcolor(uint8_t);
|
||||||
|
|
||||||
|
/* Kernel terminal - Printing function */
|
||||||
void kterm_putat(char, uint8_t, size_t, size_t);
|
void kterm_putat(char, uint8_t, size_t, size_t);
|
||||||
void kterm_put(char);
|
void kterm_put(char);
|
||||||
void kterm_write(const char*, size_t);
|
void kterm_write(const char*, size_t);
|
||||||
@ -18,6 +20,15 @@ void kterm_writestring(const char*);
|
|||||||
void kterm_scrollup();
|
void kterm_scrollup();
|
||||||
|
|
||||||
|
|
||||||
|
/* Kernel terminal - Cursor functions */
|
||||||
|
void enable_cursor (uint8_t start_cursor , uint8_t end_cursor );
|
||||||
|
void disable_cursor();
|
||||||
|
void update_cursor(int x, int y);
|
||||||
|
uint16_t get_cursor_position();
|
||||||
|
int get_cursor_x (uint16_t cursor_pos);
|
||||||
|
int get_cursor_y (uint16_t cursor_pos);
|
||||||
|
|
||||||
|
|
||||||
void printf ( const char *format, ...);
|
void printf ( const char *format, ...);
|
||||||
|
|
||||||
static void itoa (char *buf, int base, int d);
|
static void itoa (char *buf, int base, int d);
|
||||||
|
41
src/kernel/arch/i386/vga/VBE.h
Normal file
41
src/kernel/arch/i386/vga/VBE.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
#define VBE_DISPI_IOPORT_INDEX 0x01CE
|
||||||
|
#define VBE_DISPI_IOPORT_DATA 0x01CF
|
||||||
|
|
||||||
|
/* VBE index values*/
|
||||||
|
#define VBE_DISPI_INDEX_ID 0x0
|
||||||
|
#define VBE_DISPI_INDEX_XRES 0x1
|
||||||
|
#define VBE_DISPI_INDEX_YRES 0x2
|
||||||
|
#define VBE_DISPI_INDEX_BPP 0x3
|
||||||
|
#define VBE_DISPI_INDEX_ENABLE 0x4
|
||||||
|
#define VBE_DISPI_INDEX_BANK 0x5
|
||||||
|
#define VBE_DISPI_INDEX_VIRT_WIDTH 0x6
|
||||||
|
#define VBE_DISPI_INDEX_VIRT_HEIGHT 0x7
|
||||||
|
#define VBE_DISPI_INDEX_X_OFFSET 0x8
|
||||||
|
#define VBE_DISPI_INDEX_Y_OFFSET 0x9
|
||||||
|
|
||||||
|
/* BGA Version */
|
||||||
|
#define VBE_DISPI_ID5 0xB0C5
|
||||||
|
#define VBE_DISPI_ID4 0xB0C3
|
||||||
|
#define VBE_DISPI_ID3 0xB0C2
|
||||||
|
#define VBE_DISPI_ID2 0xB0C1
|
||||||
|
#define VBE_DISPI_ID1 0xB0C0
|
||||||
|
|
||||||
|
|
||||||
|
/* BGA BIT DEPTH */
|
||||||
|
#define VBE_DISPI_BPP_4 0x04
|
||||||
|
#define VBE_DISPI_BPP_8 0x08
|
||||||
|
#define VBE_DISPI_BPP_15 0x0F
|
||||||
|
#define VBE_DISPI_BPP_16 0x10
|
||||||
|
#define VBE_DISPI_BPP_24 0x18
|
||||||
|
#define VBE_DISPI_BPP_32 0x20
|
||||||
|
|
||||||
|
|
||||||
|
/*unsigned short BGAReadRegister(unsigned short IndexValue){
|
||||||
|
// outpw(VBE_DISPI_IOPORT_INDEX, IndexValue);
|
||||||
|
// return inpw (VBE_DISPI_IOPORT_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BGAIsAvailable (){
|
||||||
|
return (BGAReadRegister(VBE_DISPI_INDEX_ID) == VBE_DISPI_ID5);
|
||||||
|
}*/
|
16
src/kernel/cpu.h
Normal file
16
src/kernel/cpu.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#include <cpuid.h> // NOTE: Only available in GCC
|
||||||
|
|
||||||
|
static int get_model(){
|
||||||
|
int ebx, unused;
|
||||||
|
__cpuid(0, unused, ebx, unused, unused);
|
||||||
|
return ebx;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CPUID_FEAT_EDX_APIC = 1 << 9
|
||||||
|
};
|
||||||
|
static int check_apic (){
|
||||||
|
unsigned int eax, unused, edx;
|
||||||
|
__get_cpuid(1, &eax, &unused, &unused, &edx);
|
||||||
|
return edx & CPUID_FEAT_EDX_APIC;
|
||||||
|
}
|
@ -1,108 +1,14 @@
|
|||||||
#include "kernel.h"
|
#include "kernel.h"
|
||||||
/**
|
|
||||||
* simple delay function
|
|
||||||
**/
|
|
||||||
void delay(int t){
|
|
||||||
volatile int i,j;
|
|
||||||
for(i=0;i<t;i++)
|
|
||||||
for(j=0;j<25000;j++)
|
|
||||||
asm("NOP");
|
|
||||||
}
|
|
||||||
|
|
||||||
class Test {
|
|
||||||
public:
|
|
||||||
Test();
|
|
||||||
void printMe();
|
|
||||||
~Test();
|
|
||||||
};
|
|
||||||
|
|
||||||
Test::Test(){
|
|
||||||
kterm_writestring("Create a test object\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
void Test::printMe(){
|
|
||||||
kterm_writestring("testObject.printMe()\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
Test::~Test(){
|
|
||||||
kterm_writestring("Destroy testObject! Bye bye\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#define PORT 0x3f8
|
|
||||||
static int init_serial() {
|
|
||||||
outb(PORT + 1, 0x00); // Disable all interrupts
|
|
||||||
outb(PORT + 3, 0x80); // Enable DLAB (set baud rate divisor)
|
|
||||||
outb(PORT + 0, 0x03); // Set divisor to 3 (lo byte) 38400 baud
|
|
||||||
outb(PORT + 1, 0x00); // (hi byte)
|
|
||||||
outb(PORT + 3, 0x03); // 8 bits, no parity, one stop bit
|
|
||||||
outb(PORT + 2, 0xC7); // Enable FIFO, clear them, with 14-byte threshold
|
|
||||||
outb(PORT + 4, 0x0B); // IRQs enabled, RTS/DSR set
|
|
||||||
outb(PORT + 4, 0x1E); // Set in loopback mode, test the serial chip
|
|
||||||
outb(PORT + 0, 0xAE); // Test serial chip (send byte 0xAE and check if serial returns same byte)
|
|
||||||
|
|
||||||
// Check if serial is faulty (i.e: not same byte as sent)
|
|
||||||
if(inb(PORT + 0) != 0xAE) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If serial is not faulty set it in normal operation mode
|
|
||||||
// (not-loopback with IRQs enabled and OUT#1 and OUT#2 bits enabled)
|
|
||||||
outb(PORT + 4, 0x0F);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int is_transmit_empty() {
|
|
||||||
return inb(PORT + 5) & 0x20;
|
|
||||||
}
|
|
||||||
|
|
||||||
void write_serial(char a) {
|
|
||||||
while (is_transmit_empty() == 0);
|
|
||||||
|
|
||||||
outb(PORT,a);
|
|
||||||
}
|
|
||||||
|
|
||||||
int serial_received() {
|
|
||||||
return inb(PORT + 5) & 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
char read_serial() {
|
|
||||||
while (serial_received() == 0);
|
|
||||||
|
|
||||||
return inb(PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_serial(const char* string ){
|
|
||||||
for(size_t i = 0; i < strlen(string); i ++){
|
|
||||||
write_serial(string[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_serial(){
|
|
||||||
/** Serial test **/
|
|
||||||
kterm_writestring("Writing to COM1 serial port:");
|
|
||||||
init_serial();
|
|
||||||
write_serial('A');
|
|
||||||
write_serial('B');
|
|
||||||
write_serial('C');
|
|
||||||
write_serial('D');
|
|
||||||
write_serial('E');
|
|
||||||
|
|
||||||
char Character_received = read_serial();
|
|
||||||
kterm_writestring("\n");
|
|
||||||
kterm_writestring("received from COM 1: \n");
|
|
||||||
kterm_put(Character_received);
|
|
||||||
|
|
||||||
kterm_writestring("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
|
multiboot_info_t *mbi;
|
||||||
void early_main(unsigned long magic, unsigned long addr){
|
void early_main(unsigned long magic, unsigned long addr){
|
||||||
|
|
||||||
/** initialize terminal interface */
|
/** initialize terminal interface */
|
||||||
kterm_init();
|
kterm_init();
|
||||||
|
|
||||||
multiboot_info_t *mbi;
|
|
||||||
|
|
||||||
if (magic != MULTIBOOT_BOOTLOADER_MAGIC){
|
if (magic != MULTIBOOT_BOOTLOADER_MAGIC){
|
||||||
printf("Invalid magic number: 0x%x\n", (unsigned) magic);
|
printf("Invalid magic number: 0x%x\n", (unsigned) magic);
|
||||||
@ -199,31 +105,20 @@ extern "C" {
|
|||||||
if (CHECK_FLAG (mbi->flags, 12)){
|
if (CHECK_FLAG (mbi->flags, 12)){
|
||||||
printf("Can draw!");
|
printf("Can draw!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int cpu_model = get_model();
|
||||||
|
int local_apic = check_apic();
|
||||||
|
printf( "CPU Model: %x, Local APIC: %D\n", cpu_model, local_apic);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void kernel_main (void) {
|
void kernel_main (void) {
|
||||||
|
|
||||||
|
|
||||||
init_serial();
|
init_serial();
|
||||||
|
|
||||||
|
|
||||||
/** Setup the MMU **/
|
|
||||||
//kterm_writestring("Starting MMU...\n");
|
|
||||||
//auto mmu = MMU();
|
|
||||||
//mmu.enable();
|
|
||||||
//kterm_writestring("MMU enabled!\n");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** test interrupt handlers **/
|
|
||||||
//asm volatile ("int $0x03");
|
|
||||||
|
|
||||||
//asm volatile ("int $0x04");
|
|
||||||
|
|
||||||
while (true){
|
while (true){
|
||||||
//Read time indefinetely
|
//Read time indefinetely
|
||||||
@ -232,14 +127,8 @@ extern "C" {
|
|||||||
delay(1000);
|
delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Lets start using the serial port for debugging .. **/
|
|
||||||
// Hopefully once we go into realmode or do something that
|
|
||||||
// cause the screen to go black.. this serial comms part will give
|
|
||||||
// some situational awareness
|
|
||||||
//Serial serialbus = Serial::init();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,105 @@ extern "C" {
|
|||||||
#include "MMU.h"
|
#include "MMU.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include "time.h"
|
#include "time.h"
|
||||||
|
#include "cpu.h"
|
||||||
|
#include "arch/i386/vga/VBE.h"
|
||||||
|
#define CHECK_FLAG(flags, bit) ((flags) & (1 <<(bit)))
|
||||||
|
|
||||||
|
|
||||||
#define CHECK_FLAG(flags, bit) ((flags) & (1 <<(bit)))
|
|
||||||
|
/* This needs to be moved! */
|
||||||
|
/**
|
||||||
|
* simple delay function
|
||||||
|
**/
|
||||||
|
void delay(int t){
|
||||||
|
volatile int i,j;
|
||||||
|
for(i=0;i<t;i++)
|
||||||
|
for(j=0;j<25000;j++)
|
||||||
|
asm("NOP");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
public:
|
||||||
|
Test();
|
||||||
|
void printMe();
|
||||||
|
~Test();
|
||||||
|
};
|
||||||
|
|
||||||
|
Test::Test(){
|
||||||
|
kterm_writestring("Create a test object\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
void Test::printMe(){
|
||||||
|
kterm_writestring("testObject.printMe()\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Test::~Test(){
|
||||||
|
kterm_writestring("Destroy testObject! Bye bye\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PORT 0x3f8
|
||||||
|
static int init_serial() {
|
||||||
|
outb(PORT + 1, 0x00); // Disable all interrupts
|
||||||
|
outb(PORT + 3, 0x80); // Enable DLAB (set baud rate divisor)
|
||||||
|
outb(PORT + 0, 0x03); // Set divisor to 3 (lo byte) 38400 baud
|
||||||
|
outb(PORT + 1, 0x00); // (hi byte)
|
||||||
|
outb(PORT + 3, 0x03); // 8 bits, no parity, one stop bit
|
||||||
|
outb(PORT + 2, 0xC7); // Enable FIFO, clear them, with 14-byte threshold
|
||||||
|
outb(PORT + 4, 0x0B); // IRQs enabled, RTS/DSR set
|
||||||
|
outb(PORT + 4, 0x1E); // Set in loopback mode, test the serial chip
|
||||||
|
outb(PORT + 0, 0xAE); // Test serial chip (send byte 0xAE and check if serial returns same byte)
|
||||||
|
|
||||||
|
// Check if serial is faulty (i.e: not same byte as sent)
|
||||||
|
if(inb(PORT + 0) != 0xAE) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If serial is not faulty set it in normal operation mode
|
||||||
|
// (not-loopback with IRQs enabled and OUT#1 and OUT#2 bits enabled)
|
||||||
|
outb(PORT + 4, 0x0F);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int is_transmit_empty() {
|
||||||
|
return inb(PORT + 5) & 0x20;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_serial(char a) {
|
||||||
|
while (is_transmit_empty() == 0);
|
||||||
|
|
||||||
|
outb(PORT,a);
|
||||||
|
}
|
||||||
|
|
||||||
|
int serial_received() {
|
||||||
|
return inb(PORT + 5) & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char read_serial() {
|
||||||
|
while (serial_received() == 0);
|
||||||
|
|
||||||
|
return inb(PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_serial(const char* string ){
|
||||||
|
for(size_t i = 0; i < strlen(string); i ++){
|
||||||
|
write_serial(string[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_serial(){
|
||||||
|
/** Serial test **/
|
||||||
|
kterm_writestring("Writing to COM1 serial port:");
|
||||||
|
init_serial();
|
||||||
|
write_serial('A');
|
||||||
|
write_serial('B');
|
||||||
|
write_serial('C');
|
||||||
|
write_serial('D');
|
||||||
|
write_serial('E');
|
||||||
|
|
||||||
|
char Character_received = read_serial();
|
||||||
|
kterm_writestring("\n");
|
||||||
|
kterm_writestring("received from COM 1: \n");
|
||||||
|
kterm_put(Character_received);
|
||||||
|
|
||||||
|
kterm_writestring("\n");
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user