Stash pop

This commit is contained in:
pjht 2021-02-28 13:46:33 -06:00
parent e35fd04d1b
commit f7503e6c6f
22 changed files with 134 additions and 41 deletions

View File

@ -18,7 +18,7 @@ AR = $(shell cat psinfo/$(PLAT)/ar.txt)
NASM = $(shell cat psinfo/$(PLAT)/nasm.txt) NASM = $(shell cat psinfo/$(PLAT)/nasm.txt)
EMU = $(shell cat psinfo/$(PLAT)/emu.txt) EMU = $(shell cat psinfo/$(PLAT)/emu.txt)
CFLAGS = -Isysroot/usr/include -Wextra -Wall -Wno-unused-parameter -g -ffreestanding CFLAGS = -Isysroot/usr/include -Wextra -Wall -Wno-unused-parameter -g -ffreestanding
QFLAGS = -hda ext2.img -m 2G -boot d -cdrom os.iso -serial file:serout #-chardev socket,id=s1,port=3000,host=localhost -serial chardev:s1 QFLAGS = -m 2G -boot d -cdrom os.iso -serial file:serout #-chardev socket,id=s1,port=3000,host=localhost -serial chardev:s1
CWD = $(shell pwd) CWD = $(shell pwd)
.PHONY: sysroot .PHONY: sysroot
@ -31,19 +31,29 @@ run: os.iso
@$(EMU) $(QFLAGS) & @$(EMU) $(QFLAGS) &
tail -f serout tail -f serout
run_nobuild:
rm -f serout
touch serout
@$(EMU) $(QFLAGS) &
tail -f serout
debug: os.iso kernel/kernel.elf debug: os.iso kernel/kernel.elf
@$(EMU) -s $(QFLAGS) & @$(EMU) -s $(QFLAGS) &
gdb gdb
#gdbgui -g i386-elf-gdb --project $(CWD) #gdbgui -g i386-elf-gdb --project $(CWD)
os.iso: kernel/kernel.elf init vfs devfs vga_drv initrd_drv tar_fs pci sysroot/usr/share/man # vfs devfs initrd vga_drv initrd_drv pci os.iso: kernel/kernel.elf init vfs devfs vga_drv initrd_drv tar_fs pci ps2 sysroot/usr/share/man # vfs devfs initrd vga_drv initrd_drv pci
@cp kernel/kernel.elf sysroot/boot @cp kernel/kernel.elf sysroot/boot
@cd initrd; tar -f ../sysroot/boot/initrd.tar -c * @cd initrd; tar -f ../sysroot/boot/initrd.tar -c *
@grub-mkrescue -o $@ sysroot >/dev/null 2>/dev/null @grub-mkrescue -o $@ sysroot
crts: kernel/crt0.o crts: kernel/crt0.o
@cp $^ sysroot/usr/lib @cp $^ sysroot/usr/lib
ps2: crts libc
@cd $@ && make
@cp $@/$@ initrd/$@
init: crts libc init: crts libc
@cd $@ && make @cd $@ && make
@cp $@/$@ initrd/$@ @cp $@/$@ initrd/$@
@ -107,3 +117,8 @@ clean:
doc: $(C_SOURCES) $(C_HEADERS) doc: $(C_SOURCES) $(C_HEADERS)
@doxygen kernel/Doxyfile > /dev/null @doxygen kernel/Doxyfile > /dev/null
@doxygen libc/Doxyfile > /dev/null @doxygen libc/Doxyfile > /dev/null
install:
@echo Path to flash drive?; \
read drive; \
echo $$(drive);

View File

@ -123,5 +123,7 @@ int main() {
serial_print("Could not open the VGA device file!\n"); serial_print("Could not open the VGA device file!\n");
exit(1); exit(1);
} }
posix_spawn(NULL,"/initrd/pci",NULL,NULL,NULL,NULL); posix_spawn(NULL,"/initrd/ps2",NULL,NULL,NULL,NULL);
// posix_spawn(NULL,"/initrd/pci",NULL,NULL,NULL,NULL);
for(;;);
} }

View File

@ -5,7 +5,7 @@ TYPEDEF_HIDES_STRUCT = YES
EXTRACT_STATIC = YES EXTRACT_STATIC = YES
WARN_NO_PARAMDOC = YES WARN_NO_PARAMDOC = YES
WARN_AS_ERROR = YES WARN_AS_ERROR = YES
INPUT = /Users/peterterpstra/Desktop/projects/os/kernel INPUT = ./kernel
RECURSIVE = YES RECURSIVE = YES
EXCLUDE = kernel/cpu/x86_64 EXCLUDE = kernel/cpu/x86_64
HTML_DYNAMIC_SECTIONS = YES HTML_DYNAMIC_SECTIONS = YES

View File

@ -34,7 +34,7 @@ typedef struct {
#define IDT_ENTRIES 256 //!< Number of entries in the IDT #define IDT_ENTRIES 256 //!< Number of entries in the IDT
static idt_gate_t idt[IDT_ENTRIES]; //!< The IDT idt_gate_t idt[IDT_ENTRIES] __attribute__((aligned(4096))); //!< The IDT
static idt_register_t idt_reg; //!< The value to load into the IDTR static idt_register_t idt_reg; //!< The value to load into the IDTR
#define LOW_16(address) (uint16_t)((address) & 0xFFFF) //!< Macro to get the low 16 bits of an address #define LOW_16(address) (uint16_t)((address) & 0xFFFF) //!< Macro to get the low 16 bits of an address

View File

@ -6,7 +6,6 @@
#include "../../vga_err.h" #include "../../vga_err.h"
#include "../../address_spaces.h" #include "../../address_spaces.h"
#include "../halt.h" #include "../halt.h"
#include "../isr.h"
#include "../paging.h" #include "../paging.h"
#include "../serial.h" #include "../serial.h"
//#include "../../rpc.h" //#include "../../rpc.h"
@ -20,8 +19,15 @@
#include <sys/syscalls.h> #include <sys/syscalls.h>
#include <sys/types.h> #include <sys/types.h>
void irq_handler(registers_t* r); /**
static isr_t irq_handlers[16]; //!< Handlers for the PIC interrupts * Represents a registeted interrupt handler
*/
typedef struct {
pid_t pid; //!< The PID thast registered the handler, or 0 if it it a kernel handler
void* handler; //!< The address of the handler
} isr_handler_info;
static isr_handler_info irq_handlers[16]={0}; //!< Handlers for the PIC interrupts
void isr_install() { void isr_install() {
idt_set_gate(0,(uint32_t)isr0); idt_set_gate(0,(uint32_t)isr0);
@ -203,7 +209,7 @@ void isr_handler(registers_t* r) {
case 80: case 80:
switch (r->eax) { switch (r->eax) {
case SYSCALL_CREATEPROC: case SYSCALL_CREATEPROC:
tasking_create_task((void*)r->ebx,(void*)r->ecx,0,(void*)r->edx,(void*)r->esi,0); tasking_create_task((void*)r->ebx,(void*)r->ecx,0,(void*)r->edx,(void*)r->esi,0,0);
break; break;
case SYSCALL_YIELD: case SYSCALL_YIELD:
tasking_yield(); tasking_yield();
@ -259,7 +265,7 @@ void isr_handler(registers_t* r) {
memcpy((char*)r->ebx,initrd,initrd_sz); memcpy((char*)r->ebx,initrd,initrd_sz);
break; break;
case SYSCALL_NEW_THREAD: { case SYSCALL_NEW_THREAD: {
uint32_t tid=tasking_new_thread((void*)r->ebx,tasking_get_PID(),(void*)r->edx); uint32_t tid=tasking_new_thread((void*)r->ebx,tasking_get_PID(),(void*)r->edx,0);
if ((uint32_t*)r->ecx!=NULL) { if ((uint32_t*)r->ecx!=NULL) {
*((uint32_t*)r->ecx)=tid; *((uint32_t*)r->ecx)=tid;
} }
@ -289,7 +295,8 @@ void isr_handler(registers_t* r) {
case SYSCALL_RPC_IS_INIT: case SYSCALL_RPC_IS_INIT:
r->ecx=kernel_rpc_is_init((pid_t)r->ebx); r->ecx=kernel_rpc_is_init((pid_t)r->ebx);
break; break;
default: case SYSCALL_REGISTER_IRQ_HANDLER:
isr_register_handler((int)r->ebx,tasking_get_PID(),(void*)r->ecx);
break; break;
} }
break; break;
@ -298,11 +305,12 @@ void isr_handler(registers_t* r) {
} }
void isr_register_handler(int n,isr_t handler) { void isr_register_handler(int n,pid_t pid,void* handler) {
if (n>16) { if (n>16) {
return; return;
} }
irq_handlers[n] = handler; irq_handlers[n].pid = pid;
irq_handlers[n].handler = handler;
} }
/** /**
@ -315,12 +323,12 @@ void irq_handler(registers_t* r) {
if (r->int_no >= 40) port_byte_out(0xA0,0x20); /* slave */ if (r->int_no >= 40) port_byte_out(0xA0,0x20); /* slave */
port_byte_out(0x20,0x20); /* master */ port_byte_out(0x20,0x20); /* master */
/* Handle the interrupt in a more modular way */ /* Handle the interrupt in a more modular way */
if (irq_handlers[r->int_no-32] != NULL) { if (irq_handlers[r->int_no-32].handler!=NULL) {
isr_t handler = irq_handlers[r->int_no-32]; isr_handler_info info = irq_handlers[r->int_no-32];
if((uint32_t)handler<32768) { if(info.pid==0) {
kernel_rpc_call((pid_t)handler,"irq_handler",NULL,NULL); ((isr_t)info.handler)(r);
} else { } else {
handler(r); tasking_new_thread(info.handler,info.pid,(void*)(r->int_no-32),1);
} }
} }
} }

View File

@ -6,6 +6,7 @@
#define ISR_H #define ISR_H
#include <stdint.h> #include <stdint.h>
#include <sys/types.h>
/** /**
* Saved state of the CPU when an interrupt occurs * Saved state of the CPU when an interrupt occurs
@ -38,9 +39,12 @@ void isr_install();
/** /**
* Register an IRQ handler * Register an IRQ handler
* \param n the IRQ to register a handler for *
* \param handler the handler to register * If the PID is 0, the handler will be called directly, otherwise a thread will be made in the PID starting at the handler's address.
* \param n The IRQ to register a handler for
* \param pid The PID that will handle the interrupt.
* \param handler The address of the handler.
*/ */
void isr_register_handler(int n,isr_t handler); void isr_register_handler(int n,pid_t pid,void* handler);
#endif #endif

View File

@ -190,6 +190,18 @@ void unmap_pages(void* start_virt,int num_pages,int free_phys) {
} }
} }
extern int idt;
/**
* Makes the IDT readonly
*/
void paging_readonly_idt() {
void* idt_addr=&idt;
void* idt_phys=virt_to_phys(idt_addr);
map_pages(idt_addr,idt_phys,1,0,0);
invl_page(idt_addr);
}
void paging_init() { void paging_init() {
for (size_t i=0;i<NUM_KERN_FRAMES;i++) { for (size_t i=0;i<NUM_KERN_FRAMES;i++) {
pg_struct_entry* entry=&kern_page_tables[i]; pg_struct_entry* entry=&kern_page_tables[i];

View File

@ -49,7 +49,7 @@ switch_to_thread_asm:
global task_init global task_init
; Switch to usermode, given a usermode stack and EIP to switch to ; Switch to usermode and enable interrupts, given a usermode stack and EIP to switch to
task_init: task_init:
pop ecx ; ecx = user ESP pop ecx ; ecx = user ESP
@ -69,6 +69,26 @@ task_init:
push ebx ; push user EIP push ebx ; push user EIP
iret iret
global task_init_no_int
; Switch to usermode, given a usermode stack and EIP to switch to
task_init_no_int:
pop ecx ; ecx = user ESP
pop ebx ; ebx = user EIP
mov ax, 0x23 ; Load data segment selectors with the usermode data segment
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
push 0x23 ; Push SS
push ecx ; push user ESP
pushf ; push flagstrew
push 0x1B ; push CS
push ebx ; push user EIP
iret
global wait_for_unblocked_thread_asm global wait_for_unblocked_thread_asm
wait_for_unblocked_thread_asm: wait_for_unblocked_thread_asm:

View File

@ -56,7 +56,7 @@ static int new_kstack() {
return num; return num;
} }
void setup_kstack(Thread* thread,void* param1,void* param2,char kmode,void* eip) { void setup_kstack(Thread* thread,void* param1,void* param2,char kmode,void* eip,char is_irq_handler) {
size_t kstack_num=new_kstack(); size_t kstack_num=new_kstack();
if (kmode) { if (kmode) {
size_t top_idx=(1024*(kstack_num+1)); size_t top_idx=(1024*(kstack_num+1));
@ -74,7 +74,11 @@ void setup_kstack(Thread* thread,void* param1,void* param2,char kmode,void* eip)
user_stack[0]=param2; user_stack[0]=param2;
user_stack[1]=param1; user_stack[1]=param1;
}); });
if (is_irq_handler) {
kstacks[top_idx-3]=(void*)task_init_no_int;
} else {
kstacks[top_idx-3]=(void*)task_init; kstacks[top_idx-3]=(void*)task_init;
}
kstacks[top_idx-2]=(void*)user_stack; kstacks[top_idx-2]=(void*)user_stack;
kstacks[top_idx-1]=(void*)eip; kstacks[top_idx-1]=(void*)eip;
} }

View File

@ -6,6 +6,7 @@
#define ISR_H #define ISR_H
#include <stdint.h> #include <stdint.h>
#include <sys/types.h>
/** /**
* Saved state of the CPU when an interrupt occurs * Saved state of the CPU when an interrupt occurs
@ -38,9 +39,12 @@ void isr_install();
/** /**
* Register an IRQ handler * Register an IRQ handler
* \param n the IRQ to register a handler for *
* \param handler the handler to register * If the PID is 0, the handler will be called directly, otherwise a thread will be made in the PID starting at the handler's address.
* \param n The IRQ to register a handler for
* \param pid The PID that will handle the interrupt.
* \param handler The address of the handler.
*/ */
void isr_register_handler(int n,isr_t handler); void isr_register_handler(int n,pid_t pid,void* handler);
#endif #endif

View File

@ -18,6 +18,11 @@ void switch_to_thread_asm(Thread* thread);
*/ */
void task_init(); void task_init();
/**
* Initializes a usermode task without interrupts enabled
*/
void task_init_no_int();
/** /**
* An assembly helper for waiting for an unblocked thread * An assembly helper for waiting for an unblocked thread
* Starts interrupts, halts, then clears interrupts. * Starts interrupts, halts, then clears interrupts.
@ -31,8 +36,9 @@ void wait_for_unblocked_thread_asm();
* \param param2 The thread's start function second parameter * \param param2 The thread's start function second parameter
* \param kmode Whether the thread is a kernel mode thread * \param kmode Whether the thread is a kernel mode thread
* \param eip The start address of the thread * \param eip The start address of the thread
* \param is_irq_handler Whether the kernel stack is for an interrupt handler thread
*/ */
void setup_kstack(Thread* thread,void* param1,void* param2,char kmode,void* eip); void setup_kstack(Thread* thread,void* param1,void* param2,char kmode,void* eip,char is_irq_handler);
/** /**
* Frees a kernel stack so it can be used again * Frees a kernel stack so it can be used again

View File

@ -6,6 +6,8 @@
#include <math.h> #include <math.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "cpu/serial.h"
#include "cpu/halt.h"
#define KMALLOC_BMAP_SZ (((KMALLOC_SZ*1024)/4)/8) //!< The size of the kmalloc bitmap #define KMALLOC_BMAP_SZ (((KMALLOC_SZ*1024)/4)/8) //!< The size of the kmalloc bitmap
@ -85,6 +87,8 @@ void* kmalloc(size_t size) {
} }
} }
if (remaining_blks!=0) { if (remaining_blks!=0) {
serial_printf("Kmalloc fail!\n");
halt();
return NULL; return NULL;
} }
for (size_t i=0;i<num_4b_grps;i++) { for (size_t i=0;i<num_4b_grps;i++) {

View File

@ -123,6 +123,8 @@ void* pmem_alloc(int num_pages) {
} }
} }
if (remaining_blks!=0) { if (remaining_blks!=0) {
serial_printf("Out of memory! Halting!\n");
halt();
return NULL; return NULL;
} }
for (int i=0;i<num_pages;i++) { for (int i=0;i<num_pages;i++) {

View File

@ -94,7 +94,7 @@ void* kernel_rpc_call(pid_t pid,char* name,void* buf,size_t size) {
map_pages(virtaddr,physaddr,(size/PAGE_SZ)+1,1,1); map_pages(virtaddr,physaddr,(size/PAGE_SZ)+1,1,1);
}); });
} }
pid_t tid=tasking_new_thread(func->code,pid,virtaddr); pid_t tid=tasking_new_thread(func->code,pid,virtaddr,0);
tasking_set_rpc_calling_thread(pid,tid); tasking_set_rpc_calling_thread(pid,tid);
// Block the thread and wait for an unblock from rpc_return // Block the thread and wait for an unblock from rpc_return
tasking_block(THREAD_WAITING_FOR_RPC); tasking_block(THREAD_WAITING_FOR_RPC);

View File

@ -90,7 +90,7 @@ void schedule_thread(Thread* thread) {
} }
} }
void tasking_create_task(void* eip,void* address_space,char kmode,void* param1,void* param2,char isThread) { void tasking_create_task(void* eip,void* address_space,char kmode,void* param1,void* param2,char isThread,char is_irq_handler) {
if (next_pid>MAX_PROCS && !isThread) { if (next_pid>MAX_PROCS && !isThread) {
serial_printf("Failed to create a process, as 32k processes have been created already.\n"); serial_printf("Failed to create a process, as 32k processes have been created already.\n");
halt(); //Cannot ever create more than 32k processes, as I don't currently reuse PIDs. halt(); //Cannot ever create more than 32k processes, as I don't currently reuse PIDs.
@ -118,9 +118,19 @@ void tasking_create_task(void* eip,void* address_space,char kmode,void* param1,v
num_procs++; num_procs++;
} }
proc->first_thread=thread; proc->first_thread=thread;
setup_kstack(thread,param1,param2,kmode,eip); setup_kstack(thread,param1,param2,kmode,eip,is_irq_handler);
schedule_thread(thread); schedule_thread(thread);
// serial_printf("Created thread with PID %d and TID %d.\n",proc->pid,thread->tid); serial_printf("Created thread with PID %d and TID %d.\n",proc->pid,thread->tid);
serial_printf("Structure values:\n");
serial_printf("kernel_esp=%x\n",thread->kernel_esp);
serial_printf("kernel_esp_top=%x\n",thread->kernel_esp_top);
serial_printf("address_space=%x\n",thread->address_space);
serial_printf("tid=%d\n",thread->tid);
serial_printf("state=%d\n",thread->state);
serial_printf("next_thread_in_process=%x\n",thread->next_thread_in_process);
serial_printf("next_ready_to_run=%x\n",thread->next_ready_to_run);
serial_printf("prev_ready_to_run=%x\n",thread->prev_ready_to_run);
serial_printf("process=%x\n",thread->process);
} }
void tasking_init() { void tasking_init() {
@ -128,7 +138,7 @@ void tasking_init() {
memset(&processes[i],0,sizeof(Process)); memset(&processes[i],0,sizeof(Process));
} }
tasking_create_task(NULL,get_address_space(),1,NULL,NULL,0); tasking_create_task(NULL,get_address_space(),1,NULL,NULL,0,0);
} }
char tasking_is_privleged() { char tasking_is_privleged() {
@ -147,8 +157,8 @@ int* tasking_get_errno_address() {
return &current_thread->errno; return &current_thread->errno;
} }
pid_t tasking_new_thread(void* start,pid_t pid,void* param) { pid_t tasking_new_thread(void* start,pid_t pid,void* param,char is_irq_handler) {
tasking_create_task(start,NULL,0,param,(void*)pid,1); tasking_create_task(start,NULL,0,param,(void*)pid,1,is_irq_handler);
return processes[pid].first_thread->tid; return processes[pid].first_thread->tid;
} }
@ -389,7 +399,6 @@ void* tasking_get_rpc_ret_buf() {
void tasking_thread_exit() { void tasking_thread_exit() {
tasking_block(THREAD_EXITED); tasking_block(THREAD_EXITED);
free_kstack(current_thread->kernel_esp_top);
} }
char tasking_check_proc_exists(pid_t pid) { char tasking_check_proc_exists(pid_t pid) {

View File

@ -57,7 +57,7 @@ typedef struct Thread {
pid_t rpc_calling_pid; //!< The PID of the thread that called this RPC (only used for RPC handler threads) pid_t rpc_calling_pid; //!< The PID of the thread that called this RPC (only used for RPC handler threads)
pid_t rpc_calling_tid; //!< The TID of the thread that called this RPC (only used for RPC handler threads) pid_t rpc_calling_tid; //!< The TID of the thread that called this RPC (only used for RPC handler threads)
void* rpc_ret_buf; //!< The return buffer of the RPC call that the thread made void* rpc_ret_buf; //!< The return buffer of the RPC call that the thread made
} Thread; } __attribute__((packed)) Thread;
extern Thread* current_thread; extern Thread* current_thread;
@ -69,8 +69,9 @@ extern Thread* current_thread;
* \param param1 The thread's start function first parameter * \param param1 The thread's start function first parameter
* \param param2 The thread's start function second parameter * \param param2 The thread's start function second parameter
* \param isThread Whether we are creating a new process or a thread in a process. If we are creating a theead, param2_arg becomes the PID for the newly created thread. * \param isThread Whether we are creating a new process or a thread in a process. If we are creating a theead, param2_arg becomes the PID for the newly created thread.
* \param is_irq_handler Whether the new task is an irq handler task
*/ */
void tasking_create_task(void* eip,void* address_space,char kmode,void* param1,void* param2,char isThread); void tasking_create_task(void* eip,void* address_space,char kmode,void* param1,void* param2,char isThread,char is_irq_handler);
/** /**
* Initialize tasking * Initialize tasking
*/ */
@ -100,9 +101,10 @@ int* tasking_get_errno_address();
* \param start The start address of the task * \param start The start address of the task
* \param pid The PID that gets the new thread * \param pid The PID that gets the new thread
* \param param The thread's start function parameter * \param param The thread's start function parameter
* \param is_irq_handler Whether the new thread is an irq handler thread
* \return the TID of the thread * \return the TID of the thread
*/ */
pid_t tasking_new_thread(void* start,pid_t pid,void* param); pid_t tasking_new_thread(void* start,pid_t pid,void* param,char is_irq_handler);
/** /**
* Terminate the current thread * Terminate the current thread

View File

@ -19,7 +19,7 @@ void timer_init(int freq) {
} else { } else {
serial_printf("Setting PIT to %dHz using divisor of %d\n",freq,div); serial_printf("Setting PIT to %dHz using divisor of %d\n",freq,div);
} }
isr_register_handler(0,timer_handler); isr_register_handler(0,0,timer_handler);
port_byte_out(0x43,0b00110110); // Set timer 0 to lobyte/hibyte access, and mode 3 (square wave generator) port_byte_out(0x43,0b00110110); // Set timer 0 to lobyte/hibyte access, and mode 3 (square wave generator)
port_byte_out(0x40,div&0xFF); port_byte_out(0x40,div&0xFF);
port_byte_out(0x40,(div>>8)&0xFF); port_byte_out(0x40,(div>>8)&0xFF);

View File

@ -5,7 +5,7 @@ TYPEDEF_HIDES_STRUCT = YES
EXTRACT_STATIC = YES EXTRACT_STATIC = YES
WARN_NO_PARAMDOC = YES WARN_NO_PARAMDOC = YES
WARN_AS_ERROR = YES WARN_AS_ERROR = YES
INPUT = /Users/peterterpstra/Desktop/projects/os/libc INPUT = ./libc
RECURSIVE = YES RECURSIVE = YES
EXCLUDE = libc/elf.h libc/grub/multiboot2.h EXCLUDE = libc/elf.h libc/grub/multiboot2.h
HTML_DYNAMIC_SECTIONS = YES HTML_DYNAMIC_SECTIONS = YES

View File

@ -28,5 +28,6 @@
#define SYSCALL_RPC_RET 18 #define SYSCALL_RPC_RET 18
#define SYSCALL_RPC_MARK_AS_INIT 21 #define SYSCALL_RPC_MARK_AS_INIT 21
#define SYSCALL_RPC_IS_INIT 22 #define SYSCALL_RPC_IS_INIT 22
#define SYSCALL_REGISTER_IRQ_HANDLER 23
#endif #endif

Binary file not shown.

0
vga_drv/vtconsole.c Executable file → Normal file
View File

0
vga_drv/vtconsole.h Executable file → Normal file
View File