kolibrios-fun/programs/system/os/modules.inc

611 lines
21 KiB
PHP
Raw Normal View History

; Module management, non-PE-specific code.
; Works in conjuction with peloader.inc for PE-specific code.
; void* dlopen(const char* filename, int mode)
; Opens the module named filename and maps it in; returns a handle that can be
; passed to dlsym to get symbol values from it.
;
; If filename starts with '/', it is treated as an absolute file name.
; Otherwise, dlopen searches for filename in predefined locations:
; /sys/lib, /kolibrios/lib, directory of the executable file.
; The current directory is *not* searched.
;
; If the same module is loaded again with dlopen(), the same
; handle is returned. The loader maintains reference
; counts for loaded modules, so a dynamically loaded module is
; not deallocated until dlclose() has been called on it as many times
; as dlopen() has succeeded on it. Any initialization functions
; are called just once.
;
; If dlopen() fails for any reason, it returns NULL.
;
; mode is reserved and should be zero.
proc dlopen stdcall uses esi edi, file, mode
; find_module_by_name and load_module do all the work.
; We just need to acquire/release the mutex and adjust input/output.
cmp [mode], 0
jnz .invalid_mode
mutex_lock modules_mutex
mov edi, [file]
call find_module_by_name
test esi, esi
jnz .inc_refcount
call load_module
xor edi, edi
test eax, eax
jz .unlock_return
; The handle returned on success is module base address.
; Unlike pointer to MODULE struct, it can be actually useful
; for the caller as is.
mov edi, [eax+MODULE.base]
jmp .unlock_return
.inc_refcount:
inc [esi+MODULE.refcount]
mov edi, [esi+MODULE.base]
.unlock_return:
mutex_unlock modules_mutex
mov eax, edi
ret
.invalid_mode:
xor eax, eax
ret
endp
; int dlclose(void* handle)
; Decrements the reference count on the dynamically loaded module
; referred to by handle. If the reference count drops to zero,
; then the module is unloaded. All modules that were automatically loaded
; when dlopen() was invoked on the module referred to by handle are
; recursively closed in the same manner.
;
; A successful return from dlclose() does not guarantee that the
; module has been actually removed from the caller's address space.
; In addition to references resulting from explicit dlopen() calls,
; a module may have been implicitly loaded (and reference counted)
; because of dependencies in other shared objects.
; Only when all references have been released can the module be removed
; from the address space.
; On success, dlclose() returns 0; on error, it returns a nonzero value.
proc dlclose stdcall uses esi, handle
; This function uses two worker functions:
; find_module_by_addr to map handle -> MODULE,
; dereference_module for the main work.
; Aside of calling these, we should only acquire/release the mutex.
mutex_lock modules_mutex
mov ecx, [handle]
call find_module_by_addr
test esi, esi
jz .invalid_handle
call dereference_module
mutex_unlock modules_mutex
xor eax, eax
ret
.invalid_handle:
mutex_unlock modules_mutex
xor eax, eax
inc eax
ret
endp
; void* dlsym(void* handle, const char* symbol)
; Obtains address of a symbol in a module.
; On failure, returns NULL.
;
; symbol can also be a number between 0 and 0xFFFF;
; it is interpreted as an ordinal of a symbol.
; Low 64K of address space are blocked for the allocation,
; so a valid pointer cannot be less than 0x10000.
;
; handle is not validated. Passing an invalid handle can result in a crash.
proc dlsym stdcall, handle, symbol
locals
export_base dd ?
export_ptr dd ?
export_size dd ?
import_module dd 0
endl
; Again, helper functions do all the work.
; We don't need to browse list of MODULEs,
; so we don't need to acquire/release the mutex.
; Unless the function is forwarded or module name is required for error message,
; but this should be processed by get_exported_function_*.
mov eax, [handle]
call prepare_import_from_module
mov ecx, [symbol]
cmp ecx, 0x10000
jb .ordinal
mov edx, -1 ; no hint for lookup in name table
call get_exported_function_by_name
ret
.ordinal:
call get_exported_function_by_ordinal
ret
endp
; Errors happen.
; Some errors should be reported to the user. Some errors are normal.
; After the process has been initialized, we don't know what an error
; should mean - is the failed DLL absolutely required or unimportant enhancement?
; So we report an error to the caller and let it decide how to handle it.
; However, when the process is initializing, there is no one to report to,
; so we must inform the user ourselves.
; In any case, write to the debug board - it is *debug* board, after all.
;
; This function is called whenever an error occurs in the loader.
; Except errors in malloc/realloc - they shouldn't happen anyway,
; and if they happened after all, we are screwed and likely will fail anyway,
; so don't bother.
; Variable number of arguments: strings to be concatenated, end with NULL.
proc loader_say_error c uses ebx esi, first_msg, ...
; 1. Concatenate all given strings to the final error message.
; 1a. Calculate the total length.
xor ebx, ebx
lea edx, [first_msg]
.get_length:
mov ecx, [edx]
test ecx, ecx
jz .length_done
@@:
inc ebx
inc ecx
cmp byte [ecx-1], 0
jnz @b
dec ebx
add edx, 4
jmp .get_length
.length_done:
inc ebx ; terminating zero
; 1b. Allocate memory. Exit if failed.
stdcall malloc, ebx
test eax, eax
jz .nothing
mov esi, eax
; 1c. Copy data.
lea edx, [first_msg]
.copy_data:
mov ecx, [edx]
test ecx, ecx
jz .data_done
@@:
mov bl, [ecx]
test bl, bl
jz @f
mov [eax], bl
inc ecx
inc eax
jmp @b
@@:
add edx, 4
jmp .copy_data
.data_done:
mov byte [eax], 0 ; terminating zero
; 2. Print to the debug board.
mov ecx, loader_debugboard_prefix
call sys_msg_board_str
mov ecx, esi
call sys_msg_board_str
mov ecx, msg_newline
call sys_msg_board_str
; 3. If the initialization is in process, report to the user.
xor eax, eax
cmp [process_initialized], al
jnz .no_report
; Use @notify. Create structure for function 70.7 on the stack.
push eax ; to be rewritten with part of path
push eax ; to be rewritten with part of path
push eax ; reserved
push eax ; reserved
push esi ; command line
push eax ; flags: none
push 7
mov eax, 70
mov ebx, esp
mov dword [ebx+21], notify_program
call FS_SYSCALL_PTR
add esp, 28
; Ignore any errors. We can't do anything with them anyway.
.no_report:
stdcall free, esi
.nothing:
ret
endp
; When the loader is initializing the process, errors can happen.
; They should be reported to the user.
; The main executable cannot do this, it is not initialized yet.
; So we should do it ourselves.
; However, after the process has been initialized, the main
;
; Helper function that is called whenever an error is occured.
; For now, we don't expect many modules in one process.
; So, all modules are linked into a single list,
; and lookup functions simply walk the entire list.
; This should be revisited if dozens of modules would be typical.
; This structure describes one loaded PE module.
; malloc'd from the default heap,
; includes variable-sized module path in the end.
struct MODULE
; All modules are linked in the global list with head at modules_list.
next dd ?
prev dd ?
base dd ? ; base address
size dd ? ; size in memory
refcount dd ? ; reference counter
timestamp dd ? ; for bound imports
basedelta dd ? ; base address - preferred address, for bound imports
num_imports dd ? ; size of imports array
imports dd ?
; Pointer to array of pointers to MODULEs containing imported functions.
; Used to unload all dependencies when the module is unloaded.
; Contains all modules referenced by import table;
; if the module forwards some export to another module,
; then forward target is added to this array when forward source is requested.
filename dd ? ; pointer inside path array after dirname
filenamelen dd ? ; strlen(filename) + 1
path rb 0
ends
; Fills some fields in a new MODULE struct based on given PE image.
; Assumes that MODULE.path has been filled during the allocation,
; does not insert the structure in the common list, fills everything else.
; in: eax -> MODULE
; in: esi = module base
proc init_module_struct
; Straightforward initialization of all non-PE-specific fields.
lea edx, [eax+MODULE.path]
mov [eax+MODULE.filename], edx
@@:
inc edx
cmp byte [edx-1], 0
jz @f
cmp byte [edx-1], '/'
jnz @b
mov [eax+MODULE.filename], edx
jmp @b
@@:
sub edx, [eax+MODULE.filename]
mov [eax+MODULE.filenamelen], edx
xor edx, edx
mov [eax+MODULE.base], esi
mov [eax+MODULE.refcount], 1
mov [eax+MODULE.num_imports], edx
mov [eax+MODULE.imports], edx
; Let the PE-specific part do its job.
init_module_struct_pe_specific
endp
; Helper function for dlclose and resolving forwarded exports from dlsym.
; in: ecx = module base address
; out: esi -> MODULE or esi = NULL
; modules_mutex should be locked
proc find_module_by_addr
; Simple linear lookup in the list.
mov esi, [modules_list + MODULE.next]
.scan:
cmp esi, modules_list
jz .notfound
cmp ecx, [esi+MODULE.base]
jz .found
mov esi, [esi+MODULE.next]
jmp .scan
.notfound:
xor esi, esi
.found:
ret
endp
; Helper function for whenever we have a module name
; and want to check whether it is already loaded.
; in: edi -> name with or without a path
; out: esi -> MODULE or esi = NULL
; modules_mutex should be locked
proc find_module_by_name uses edi
; 1. Skip the path, if it is present.
; eax = current pointer,
; edi is updated whenever the previous character is '/'
mov eax, edi
.find_basename:
cmp byte [eax], 0
jz .found_basename
inc eax
cmp byte [eax-1], '/'
jnz .find_basename
mov edi, eax
jmp .find_basename
.found_basename:
; 2. Simple linear lookup in the list.
mov eax, [modules_list + MODULE.next]
.scan:
cmp eax, modules_list
jz .notfound
; For every module, compare base names ignoring paths.
push edi
mov esi, [eax+MODULE.filename]
mov ecx, [eax+MODULE.filenamelen]
repz cmpsb
pop edi
jz .found
mov eax, [eax+MODULE.next]
jmp .scan
.found:
mov esi, eax
ret
.notfound:
xor esi, esi
ret
endp
; Called when some module is implicitly loaded by another module,
; either due to a record in import table,
; or because some exported function forwards to another module.
; Checks whether the target module has already been referenced
; by the source module. The first reference is passed down
; to load_module increasing refcount of the target and possibly
; loading it if not yet, subsequent references just return
; without modifying refcount.
; We don't actually need to deduplicate DLLs from import table
; as long as we decrement refcount on unload the same number of times
; that we have incremented it on load.
; However, we need to keep track of references to forward targets,
; and we don't want to scan the entire export table and load all forward
; targets just in case some of those would be useful,
; so load them on-demand first time and ignore subsequential references.
; To be consistent, do the same for import table too.
;
; in: esi -> source MODULE struct
; in: edi -> target module name
; out: eax -> imported MODULE, 0 on error
; modules_mutex should be locked
proc load_imported_module uses edi
; 1. Find the target module in the loaded modules list.
; If not found, go to 5.
push esi
call find_module_by_name
test esi, esi
mov eax, esi
pop esi
jz .load
; 2. The module has been already loaded.
; Now check whether it is already stored in imports array.
; If yes, just return without doing anything.
mov edi, [esi+MODULE.imports]
mov ecx, [esi+MODULE.num_imports]
test ecx, ecx
jz .newref
repnz scasd
jz .nothing
.newref:
; The module is loaded, but not by us.
; 3. Increment the reference counter of the target.
inc [eax+MODULE.refcount]
.add_to_imports:
; 4. Add the new pointer to the imports array.
; 4a. Check whether there is place in the array.
; If so, go to 4c.
; We don't want to reallocate too often, since reallocation
; may involve copying our data to a new place.
; We always reserve space that is a power of two; in this way,
; the wasted space is never greater than the used space,
; and total time of copying the data is O(number of modules).
; The last fact is not really important right now,
; since the current implementation of step 2 makes everything
; quadratic and the number of modules is very small anyway,
; but since this enhancement costs only a few instructions, why not?
mov edi, eax
; X is a power of two or zero if and only if (X and (X - 1)) is zero
mov ecx, [esi+MODULE.num_imports]
lea edx, [ecx-1]
test ecx, edx
jnz .has_space
; 4b. Reallocate the imports array:
; if the current size is zero, allocate 1 item,
; otherwise double number of items.
; Item size is 4 bytes.
lea ecx, [ecx*8]
test ecx, ecx
jnz @f
mov ecx, 4
@@:
stdcall realloc, [esi+MODULE.imports], ecx
test eax, eax
jz .realloc_failed
mov [esi+MODULE.imports], eax
mov ecx, [esi+MODULE.num_imports]
.has_space:
; 4c. Append pointer to the target MODULE to imports array.
mov eax, [esi+MODULE.imports]
mov [eax+ecx*4], edi
inc [esi+MODULE.num_imports]
mov eax, edi
.nothing:
ret
.load:
; 5. This is a totally new module. Load it.
call load_module
; On error, return it to the caller. On success, go to 4.
test eax, eax
jz .nothing
jmp .add_to_imports
.realloc_failed:
; Out of memory for a couple of dwords? Should not happen.
; Dereference the target referenced by step 3 or 5
; and return error to the caller.
push esi
mov esi, edi
call dereference_module
pop esi
xor eax, eax
ret
endp
; Helper procedure for load_module.
; Allocates MODULE structure for (given path) + (module name),
; calls the kernel to map it,
; on success, fills the MODULE structure.
; in: edi -> module name
; in: ebx = strlen(filename) + 1
proc try_map_module uses ebx esi, path_ptr, path_len
; 1. Allocate MODULE structure.
mov eax, [path_len]
lea eax, [eax+ebx+MODULE.path]
stdcall malloc, eax
test eax, eax
jz .nothing
; 2. Create the full name of module in MODULE structure:
; concatenate module path, if given, and module name.
mov ecx, [path_len]
mov esi, [path_ptr]
push edi
lea edi, [eax+MODULE.path]
rep movsb
mov ecx, ebx
mov esi, [esp]
rep movsb
pop edi
mov esi, eax
; 3. Call the kernel to map the module.
lea ecx, [eax+MODULE.path]
mov eax, 68
mov ebx, 28
call FS_SYSCALL_PTR
cmp eax, -0x1000
ja .failed
; 4. On success, fill the rest of MODULE structure and return it.
xchg eax, esi
call init_module_struct
ret
.failed:
; On failure, undo allocation at step 1 and return zero.
stdcall free, esi
xor eax, eax
.nothing:
ret
endp
; Worker procedure for loading a new module.
; Does not check whether the module has been already loaded;
; find_module_by_name should be called beforehand.
; in: edi -> filename
; out: eax -> MODULE or 0
; modules_mutex should be locked
proc load_module uses ebx esi ebp
; 1. Map the module.
; 1a. Prepare for try_map_module: calculate length of the name.
mov ebx, edi
@@:
inc ebx
cmp byte [ebx-1], 0
jnz @b
sub ebx, edi
; 1b. Check whether the given path is absolute.
; If so, proceed to 1c. If not, go to 1d.
cmp byte [edi], '/'
jnz .relative
; 1c. The given path is absolute. Use it as is. Don't try any other paths.
stdcall try_map_module, 0, 0
test eax, eax
jnz .loaded_ok
ccall loader_say_error, msg_cannot_open, edi, 0
jmp .load_failed
.relative:
; 1d. The given path is relative.
; Try /sys/lib/, /kolibrios/lib/ and path to executable
; in this order.
stdcall try_map_module, module_path1, module_path1.size
test eax, eax
jnz .loaded_ok
stdcall try_map_module, module_path2, module_path2.size
test eax, eax
jnz .loaded_ok
; Note: we assume that the executable is always the first module in the list.
mov eax, [modules_list + MODULE.next]
mov ecx, [eax+MODULE.filename]
add eax, MODULE.path
mov esi, eax
sub ecx, eax
stdcall try_map_module, eax, ecx
test eax, eax
jnz .loaded_ok
mov ebx, dword [esi+MODULE.filename-MODULE.path]
movzx eax, byte [ebx]
mov byte [ebx], 0
push eax
ccall loader_say_error, msg_cannot_open, edi, msg_paths_begin, esi, 0
pop eax
mov byte [ebx], al
.load_failed:
xor eax, eax
ret
.loaded_ok:
; Module has been mapped.
; MODULE structure has been initialized, but not yet inserted in the common list.
; 2. Insert the MODULE structure in the end of the common list.
mov esi, eax
mov eax, [modules_list+MODULE.prev]
mov [eax+MODULE.next], esi
mov [esi+MODULE.prev], eax
mov [modules_list+MODULE.prev], esi
mov [esi+MODULE.next], modules_list
; 3. Call PE-specific code to initialize the mapped module.
push esi
push edi ; for messages in fixup_pe_relocations
mov esi, [esi+MODULE.base]
call fixup_pe_relocations
pop ecx
pop esi
jc .fail_unload
call resolve_pe_imports
test eax, eax
jnz .fail_unload
mov eax, esi
ret
.fail_unload:
call dereference_module
xor eax, eax
ret
endp
; Worker procedure for unloading a module.
; Drops one reference to the module; if it was the last one,
; unloads the module and all referenced modules recursively.
; in: esi -> MODULE struct
; modules_mutex should be locked
proc dereference_module
; 1. Decrement reference counter.
; If the decremented value is nonzero, exit.
dec [esi+MODULE.refcount]
jnz .nothing
; 2. Remove the module from the common list.
mov eax, [esi+MODULE.prev]
mov edx, [esi+MODULE.next]
mov [eax+MODULE.next], edx
mov [edx+MODULE.prev], eax
; 3. Recursively unload dependencies.
cmp [esi+MODULE.num_imports], 0
jz .import_deref_done
.import_deref_loop:
mov eax, [esi+MODULE.num_imports]
push esi
mov esi, [esi+MODULE.imports]
mov esi, [esi+(eax-1)*4]
call dereference_module
pop esi
dec [esi+MODULE.num_imports]
jnz .import_deref_loop
.import_deref_done:
stdcall free, [esi+MODULE.imports] ; free(NULL) is ok
; 4. Unmap the module.
push ebx
mov eax, 68
mov ebx, 29
mov ecx, [esi+MODULE.base]
call FS_SYSCALL_PTR
pop ebx
; 5. Free the MODULE struct.
stdcall free, esi
.nothing:
ret
endp