forked from Bananymous/banan-os
Kernel: Add argc and argv to process entry
This commit is contained in:
parent
e0a7e242f8
commit
e0a72defa2
|
@ -25,13 +25,14 @@ continue_thread:
|
||||||
movq $0, %rax
|
movq $0, %rax
|
||||||
jmp *%rsi
|
jmp *%rsi
|
||||||
|
|
||||||
# void thread_jump_userspace(uint64_t rsp, uint64_t rip)
|
# void thread_userspace_trampoline(uint64_t rsp, uint64_t rip, int argc, char** argv)
|
||||||
.global thread_jump_userspace
|
.global thread_userspace_trampoline
|
||||||
thread_jump_userspace:
|
thread_userspace_trampoline:
|
||||||
pushq $0x23
|
pushq $0x23
|
||||||
pushq %rdi
|
pushq %rdi
|
||||||
pushfq
|
pushfq
|
||||||
pushq $0x1B
|
pushq $0x1B
|
||||||
pushq %rsi
|
pushq %rsi
|
||||||
movq $0, %rdi
|
movq %rdx, %rdi
|
||||||
|
movq %rcx, %rsi
|
||||||
iretq
|
iretq
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <BAN/NoCopyMove.h>
|
#include <BAN/NoCopyMove.h>
|
||||||
#include <BAN/RefPtr.h>
|
#include <BAN/RefPtr.h>
|
||||||
|
#include <kernel/Memory/Heap.h>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
@ -27,11 +28,9 @@ namespace Kernel
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static BAN::ErrorOr<Thread*> create(entry_t, void*, Process*);
|
static BAN::ErrorOr<Thread*> create(entry_t, void*, Process*);
|
||||||
static BAN::ErrorOr<Thread*> create_userspace(uintptr_t, Process*);
|
static BAN::ErrorOr<Thread*> create_userspace(uintptr_t, Process*, int, char**);
|
||||||
~Thread();
|
~Thread();
|
||||||
|
|
||||||
void jump_userspace(uintptr_t rip);
|
|
||||||
|
|
||||||
pid_t tid() const { return m_tid; }
|
pid_t tid() const { return m_tid; }
|
||||||
|
|
||||||
void set_rsp(uintptr_t rsp) { m_rsp = rsp; validate_stack(); }
|
void set_rsp(uintptr_t rsp) { m_rsp = rsp; validate_stack(); }
|
||||||
|
@ -44,7 +43,7 @@ namespace Kernel
|
||||||
void terminate() { m_state = State::Terminating; }
|
void terminate() { m_state = State::Terminating; }
|
||||||
|
|
||||||
uintptr_t stack_base() const { return (uintptr_t)m_stack_base; }
|
uintptr_t stack_base() const { return (uintptr_t)m_stack_base; }
|
||||||
size_t stack_size() const { return m_stack_size; }
|
size_t stack_size() const { return m_is_userspace ? m_userspace_stack_size : m_kernel_stack_size; }
|
||||||
|
|
||||||
uintptr_t interrupt_stack_base() const { return (uintptr_t)m_interrupt_stack; }
|
uintptr_t interrupt_stack_base() const { return (uintptr_t)m_interrupt_stack; }
|
||||||
uintptr_t interrupt_stack_size() const { return m_interrupt_stack_size; }
|
uintptr_t interrupt_stack_size() const { return m_interrupt_stack_size; }
|
||||||
|
@ -57,23 +56,33 @@ namespace Kernel
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Thread(pid_t tid, Process*);
|
Thread(pid_t tid, Process*);
|
||||||
|
|
||||||
void validate_stack() const;
|
|
||||||
|
|
||||||
BAN::ErrorOr<void> initialize(entry_t, void*);
|
|
||||||
void on_exit();
|
void on_exit();
|
||||||
|
|
||||||
|
void validate_stack() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t m_stack_size = 4096 * 1;
|
struct userspace_entry_t
|
||||||
static constexpr size_t m_interrupt_stack_size = 4096;
|
{
|
||||||
void* m_interrupt_stack { nullptr };
|
uintptr_t entry;
|
||||||
void* m_stack_base { nullptr };
|
int argc { 0 };
|
||||||
|
char** argv { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t m_kernel_stack_size = PAGE_SIZE * 1;
|
||||||
|
static constexpr size_t m_userspace_stack_size = PAGE_SIZE * 1;
|
||||||
|
static constexpr size_t m_interrupt_stack_size = PAGE_SIZE;
|
||||||
|
vaddr_t m_interrupt_stack { 0 };
|
||||||
|
vaddr_t m_stack_base { 0 };
|
||||||
uintptr_t m_rip { 0 };
|
uintptr_t m_rip { 0 };
|
||||||
uintptr_t m_rsp { 0 };
|
uintptr_t m_rsp { 0 };
|
||||||
const pid_t m_tid { 0 };
|
const pid_t m_tid { 0 };
|
||||||
State m_state { State::NotStarted };
|
State m_state { State::NotStarted };
|
||||||
Process* m_process { nullptr };
|
Process* m_process { nullptr };
|
||||||
bool m_in_syscall { false };
|
bool m_in_syscall { false };
|
||||||
|
bool m_is_userspace { false };
|
||||||
|
|
||||||
|
userspace_entry_t m_userspace_entry;
|
||||||
|
|
||||||
friend class Scheduler;
|
friend class Scheduler;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include <kernel/CriticalScope.h>
|
#include <kernel/CriticalScope.h>
|
||||||
#include <kernel/Memory/FixedWidthAllocator.h>
|
#include <kernel/Memory/FixedWidthAllocator.h>
|
||||||
#include <kernel/Memory/MMU.h>
|
#include <kernel/Memory/MMU.h>
|
||||||
|
#include <kernel/Memory/MMUScope.h>
|
||||||
#include <kernel/Process.h>
|
#include <kernel/Process.h>
|
||||||
|
|
||||||
namespace Kernel
|
namespace Kernel
|
||||||
|
@ -20,9 +21,7 @@ namespace Kernel
|
||||||
m_allocated_pages = m_mmu.get_free_page();
|
m_allocated_pages = m_mmu.get_free_page();
|
||||||
m_mmu.map_page_at(allocated_pages_paddr, m_allocated_pages, MMU::Flags::ReadWrite | MMU::Flags::Present);
|
m_mmu.map_page_at(allocated_pages_paddr, m_allocated_pages, MMU::Flags::ReadWrite | MMU::Flags::Present);
|
||||||
|
|
||||||
CriticalScope _;
|
MMUScope _(m_mmu);
|
||||||
|
|
||||||
m_mmu.load();
|
|
||||||
|
|
||||||
memset((void*)m_nodes_page, 0, PAGE_SIZE);
|
memset((void*)m_nodes_page, 0, PAGE_SIZE);
|
||||||
memset((void*)m_allocated_pages, 0, PAGE_SIZE);
|
memset((void*)m_allocated_pages, 0, PAGE_SIZE);
|
||||||
|
@ -38,8 +37,6 @@ namespace Kernel
|
||||||
|
|
||||||
m_free_list = node_table;
|
m_free_list = node_table;
|
||||||
m_used_list = nullptr;
|
m_used_list = nullptr;
|
||||||
|
|
||||||
Process::current().mmu().load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FixedWidthAllocator::~FixedWidthAllocator()
|
FixedWidthAllocator::~FixedWidthAllocator()
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include <kernel/FS/VirtualFileSystem.h>
|
#include <kernel/FS/VirtualFileSystem.h>
|
||||||
#include <kernel/LockGuard.h>
|
#include <kernel/LockGuard.h>
|
||||||
#include <kernel/Memory/Heap.h>
|
#include <kernel/Memory/Heap.h>
|
||||||
|
#include <kernel/Memory/MMUScope.h>
|
||||||
#include <kernel/Process.h>
|
#include <kernel/Process.h>
|
||||||
#include <kernel/Scheduler.h>
|
#include <kernel/Scheduler.h>
|
||||||
#include <LibELF/ELF.h>
|
#include <LibELF/ELF.h>
|
||||||
|
@ -87,11 +88,9 @@ namespace Kernel
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
CriticalScope _;
|
MMUScope _(process->mmu());
|
||||||
process->mmu().load();
|
|
||||||
memcpy((void*)elf_program_header.p_vaddr, elf->data() + elf_program_header.p_offset, elf_program_header.p_filesz);
|
memcpy((void*)elf_program_header.p_vaddr, elf->data() + elf_program_header.p_offset, elf_program_header.p_filesz);
|
||||||
memset((void*)(elf_program_header.p_vaddr + elf_program_header.p_filesz), 0, elf_program_header.p_memsz - elf_program_header.p_filesz);
|
memset((void*)(elf_program_header.p_vaddr + elf_program_header.p_filesz), 0, elf_program_header.p_memsz - elf_program_header.p_filesz);
|
||||||
Process::current().mmu().load();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +99,16 @@ namespace Kernel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* thread = MUST(Thread::create_userspace(elf_file_header.e_entry, process));
|
char** argv = nullptr;
|
||||||
|
{
|
||||||
|
MMUScope _(process->mmu());
|
||||||
|
argv = (char**)MUST(process->allocate(sizeof(char**) * 1));
|
||||||
|
argv[0] = (char*)MUST(process->allocate(path.size() + 1));
|
||||||
|
memcpy(argv[0], path.data(), path.size());
|
||||||
|
argv[0][path.size()] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* thread = MUST(Thread::create_userspace(elf_file_header.e_entry, process, 1, argv));
|
||||||
process->add_thread(thread);
|
process->add_thread(thread);
|
||||||
|
|
||||||
delete elf;
|
delete elf;
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
#include <BAN/Errors.h>
|
#include <BAN/Errors.h>
|
||||||
|
#include <kernel/CriticalScope.h>
|
||||||
#include <kernel/InterruptController.h>
|
#include <kernel/InterruptController.h>
|
||||||
#include <kernel/Memory/kmalloc.h>
|
#include <kernel/Memory/kmalloc.h>
|
||||||
#include <kernel/Process.h>
|
#include <kernel/Process.h>
|
||||||
#include <kernel/Scheduler.h>
|
#include <kernel/Scheduler.h>
|
||||||
#include <kernel/Thread.h>
|
#include <kernel/Thread.h>
|
||||||
|
|
||||||
#define PAGE_SIZE 4096
|
|
||||||
|
|
||||||
namespace Kernel
|
namespace Kernel
|
||||||
{
|
{
|
||||||
|
|
||||||
extern "C" void thread_jump_userspace(uintptr_t rsp, uintptr_t rip);
|
extern "C" void thread_userspace_trampoline(uint64_t rsp, uint64_t rip, int argc, char** argv);
|
||||||
|
|
||||||
template<size_t size, typename T>
|
template<size_t size, typename T>
|
||||||
static void write_to_stack(uintptr_t& rsp, const T& value)
|
static void write_to_stack(uintptr_t& rsp, const T& value)
|
||||||
|
@ -19,32 +18,66 @@ namespace Kernel
|
||||||
memcpy((void*)rsp, (void*)&value, size);
|
memcpy((void*)rsp, (void*)&value, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static pid_t s_next_tid = 1;
|
||||||
|
|
||||||
BAN::ErrorOr<Thread*> Thread::create(entry_t entry, void* data, Process* process)
|
BAN::ErrorOr<Thread*> Thread::create(entry_t entry, void* data, Process* process)
|
||||||
{
|
{
|
||||||
static pid_t next_tid = 1;
|
// Create the thread object
|
||||||
auto* thread = new Thread(next_tid++, process);
|
Thread* thread = new Thread(s_next_tid++, process);
|
||||||
if (thread == nullptr)
|
if (thread == nullptr)
|
||||||
return BAN::Error::from_errno(ENOMEM);
|
return BAN::Error::from_errno(ENOMEM);
|
||||||
TRY(thread->initialize(entry, data));
|
|
||||||
|
// Initialize stack and registers
|
||||||
|
thread->m_stack_base = (vaddr_t)kmalloc(m_kernel_stack_size, PAGE_SIZE);
|
||||||
|
if (thread->m_stack_base == 0)
|
||||||
|
return BAN::Error::from_errno(ENOMEM);
|
||||||
|
thread->m_rsp = (uintptr_t)thread->m_stack_base + m_kernel_stack_size;
|
||||||
|
thread->m_rip = (uintptr_t)entry;
|
||||||
|
|
||||||
|
// Initialize stack for returning
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, thread);
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, &Thread::on_exit);
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, data);
|
||||||
|
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
BAN::ErrorOr<Thread*> Thread::create_userspace(uintptr_t entry, Process* process)
|
BAN::ErrorOr<Thread*> Thread::create_userspace(uintptr_t entry, Process* process, int argc, char** argv)
|
||||||
{
|
{
|
||||||
Thread* thread = TRY(Thread::create(
|
// Create the thread object
|
||||||
[](void* entry)
|
Thread* thread = new Thread(s_next_tid++, process);
|
||||||
{
|
if (thread == nullptr)
|
||||||
Thread::current().jump_userspace((uintptr_t)entry);
|
|
||||||
ASSERT_NOT_REACHED();
|
|
||||||
}, (void*)entry, process
|
|
||||||
));
|
|
||||||
thread->m_interrupt_stack = kmalloc(m_interrupt_stack_size, PAGE_SIZE);
|
|
||||||
if (thread->m_interrupt_stack == nullptr)
|
|
||||||
{
|
|
||||||
delete thread;
|
|
||||||
return BAN::Error::from_errno(ENOMEM);
|
return BAN::Error::from_errno(ENOMEM);
|
||||||
}
|
thread->m_is_userspace = true;
|
||||||
process->mmu().identity_map_range(thread->stack_base(), thread->stack_size(), MMU::Flags::UserSupervisor | MMU::Flags::ReadWrite | MMU::Flags::Present);
|
|
||||||
|
// Allocate stack
|
||||||
|
thread->m_stack_base = (uintptr_t)kmalloc(m_userspace_stack_size, PAGE_SIZE);
|
||||||
|
ASSERT(thread->m_stack_base);
|
||||||
|
process->mmu().identity_map_range(thread->m_stack_base, m_userspace_stack_size, MMU::Flags::UserSupervisor | MMU::Flags::ReadWrite | MMU::Flags::Present);
|
||||||
|
|
||||||
|
// Allocate interrupt stack
|
||||||
|
thread->m_interrupt_stack = (vaddr_t)kmalloc(m_interrupt_stack_size, PAGE_SIZE);
|
||||||
|
ASSERT(thread->m_interrupt_stack);
|
||||||
|
|
||||||
|
thread->m_userspace_entry = { .entry = entry, .argc = argc, .argv = argv };
|
||||||
|
|
||||||
|
// Setup registers and entry
|
||||||
|
static entry_t entry_trampoline(
|
||||||
|
[](void*)
|
||||||
|
{
|
||||||
|
userspace_entry_t& entry = Thread::current().m_userspace_entry;
|
||||||
|
thread_userspace_trampoline(Thread::current().rsp(), entry.entry, entry.argc, entry.argv);
|
||||||
|
ASSERT_NOT_REACHED();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
thread->m_rsp = thread->m_stack_base + m_userspace_stack_size;
|
||||||
|
thread->m_rip = (uintptr_t)entry_trampoline;
|
||||||
|
|
||||||
|
// Setup stack for returning
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, thread);
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, &Thread::on_exit);
|
||||||
|
write_to_stack<sizeof(void*)>(thread->m_rsp, nullptr);
|
||||||
|
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,32 +96,13 @@ namespace Kernel
|
||||||
return *m_process;
|
return *m_process;
|
||||||
}
|
}
|
||||||
|
|
||||||
BAN::ErrorOr<void> Thread::initialize(entry_t entry, void* data)
|
|
||||||
{
|
|
||||||
m_stack_base = kmalloc(m_stack_size, PAGE_SIZE);
|
|
||||||
if (m_stack_base == nullptr)
|
|
||||||
return BAN::Error::from_errno(ENOMEM);
|
|
||||||
m_rsp = (uintptr_t)m_stack_base + m_stack_size;
|
|
||||||
m_rip = (uintptr_t)entry;
|
|
||||||
|
|
||||||
write_to_stack<sizeof(void*)>(m_rsp, this);
|
|
||||||
write_to_stack<sizeof(void*)>(m_rsp, &Thread::on_exit);
|
|
||||||
write_to_stack<sizeof(void*)>(m_rsp, data);
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread::~Thread()
|
Thread::~Thread()
|
||||||
{
|
{
|
||||||
dprintln("thread {} ({}) exit", tid(), m_process->pid());
|
dprintln("thread {} ({}) exit", tid(), m_process->pid());
|
||||||
if (m_interrupt_stack)
|
|
||||||
kfree(m_interrupt_stack);
|
|
||||||
kfree(m_stack_base);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Thread::jump_userspace(uintptr_t rip)
|
if (m_interrupt_stack)
|
||||||
{
|
kfree((void*)m_interrupt_stack);
|
||||||
thread_jump_userspace(rsp(), rip);
|
kfree((void*)m_stack_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Thread::validate_stack() const
|
void Thread::validate_stack() const
|
||||||
|
|
|
@ -180,7 +180,7 @@ static void init2(void* tty1)
|
||||||
|
|
||||||
((TTY*)tty1)->initialize_device();
|
((TTY*)tty1)->initialize_device();
|
||||||
|
|
||||||
MUST(Process::create_userspace("/usr/bin/test"sv));
|
MUST(Process::create_userspace("/usr/bin/cat"sv));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Process::create_kernel(
|
Process::create_kernel(
|
||||||
|
|
|
@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.26)
|
||||||
project(userspace CXX)
|
project(userspace CXX)
|
||||||
|
|
||||||
set(USERSPACE_PROJECTS
|
set(USERSPACE_PROJECTS
|
||||||
|
cat
|
||||||
test
|
test
|
||||||
yes
|
yes
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
cmake_minimum_required(VERSION 3.26)
|
||||||
|
|
||||||
|
project(cat CXX)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(cat ${SOURCES})
|
||||||
|
target_compile_options(cat PUBLIC -O2 -g)
|
||||||
|
add_dependencies(cat libc-install)
|
||||||
|
target_link_options(cat PUBLIC -nodefaultlibs -lc)
|
||||||
|
|
||||||
|
add_custom_target(cat-install
|
||||||
|
COMMAND cp ${CMAKE_CURRENT_BINARY_DIR}/cat ${BANAN_BIN}/
|
||||||
|
DEPENDS cat
|
||||||
|
)
|
|
@ -0,0 +1,50 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
bool cat_file(FILE* fp)
|
||||||
|
{
|
||||||
|
char buffer[1024];
|
||||||
|
size_t n_read;
|
||||||
|
while ((n_read = fread(buffer, 1, sizeof(buffer) - 1, fp)) > 0)
|
||||||
|
{
|
||||||
|
buffer[n_read] = '\0';
|
||||||
|
fputs(buffer, stdout);
|
||||||
|
}
|
||||||
|
if (ferror(fp))
|
||||||
|
{
|
||||||
|
perror("fread");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
printf("argc %d, argv %p\n", argc, argv);
|
||||||
|
for (int i = 0; i < argc; i++)
|
||||||
|
printf("%s\n", argv[i]);
|
||||||
|
|
||||||
|
if (argc > 1)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < argc; i++)
|
||||||
|
{
|
||||||
|
FILE* fp = fopen(argv[i], "r");
|
||||||
|
if (fp == nullptr)
|
||||||
|
{
|
||||||
|
perror(argv[i]);
|
||||||
|
ret = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cat_file(fp))
|
||||||
|
ret = 1;
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret = cat_file(stdin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
|
@ -1,36 +1,12 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
#define ERROR(msg) { perror(msg); return 1; }
|
#define ERROR(msg) { perror(msg); return 1; }
|
||||||
#define BUF_SIZE 1024
|
#define BUF_SIZE 1024
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
printf("%.2e\n", 1230.0);
|
|
||||||
printf("%.2e\n", 123.0);
|
|
||||||
printf("%.2e\n", 12.3);
|
|
||||||
printf("%.2e\n", 1.23);
|
|
||||||
printf("%.2e\n", 0.123);
|
|
||||||
printf("%.2e\n", 0.0123);
|
|
||||||
printf("%.2e\n", 0.00123);
|
|
||||||
|
|
||||||
printf("%e\n", 123.456);
|
|
||||||
printf("%.2e\n", 123.456);
|
|
||||||
printf("%.0e\n", 123.456);
|
|
||||||
printf("%#.0e\n", 123.456);
|
|
||||||
|
|
||||||
printf("%e\n", -123.456);
|
|
||||||
printf("%.2e\n", -123.456);
|
|
||||||
printf("%.0e\n", -123.456);
|
|
||||||
printf("%#.0e\n", -123.456);
|
|
||||||
|
|
||||||
printf("%e\n", 0.0);
|
|
||||||
printf("%e\n", -0.0);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
FILE* fp = fopen("/usr/include/stdio.h", "r");
|
FILE* fp = fopen("/usr/include/stdio.h", "r");
|
||||||
if (fp == NULL)
|
if (fp == NULL)
|
||||||
ERROR("fopen");
|
ERROR("fopen");
|
||||||
|
|
Loading…
Reference in New Issue