0,0 → 1,610 |
; 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: |
; /rd/1/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 /rd/1/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 |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |