toaruos/linker/linker.c

694 lines
16 KiB
C
Raw Normal View History

2016-11-21 13:17:54 +03:00
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <alloca.h>
#include <unistd.h>
#include <syscall.h>
#include <sys/stat.h>
2016-11-21 13:17:54 +03:00
#define TRACE_APP_NAME "ld.so"
2016-12-01 17:24:35 +03:00
#define TRACE_LD(...) do { if (__trace_ld) { TRACE(__VA_ARGS__); } } while (0)
static int __trace_ld = 0;
2016-11-21 13:17:54 +03:00
#include "../kernel/include/elf.h"
#include "../userspace/lib/trace.h"
2016-11-29 15:30:15 +03:00
#include "../userspace/lib/list.c"
#include "../userspace/lib/hashmap.c"
typedef int (*entry_point_t)(int, char *[], char**);
2016-11-21 13:17:54 +03:00
extern char end[];
2016-12-01 17:24:35 +03:00
static hashmap_t * dumb_symbol_table;
static hashmap_t * glob_dat;
static hashmap_t * objects_map;
2016-12-01 17:24:35 +03:00
typedef struct elf_object {
2016-11-21 13:17:54 +03:00
FILE * file;
2016-12-01 17:24:35 +03:00
/* Full copy of the header. */
2016-11-21 13:17:54 +03:00
Elf32_Header header;
2016-12-01 17:24:35 +03:00
/* Pointers to loaded stuff */
char * string_table;
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
char * dyn_string_table;
size_t dyn_string_table_size;
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
Elf32_Sym * dyn_symbol_table;
size_t dyn_symbol_table_size;
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
Elf32_Dyn * dynamic;
Elf32_Word * dyn_hash;
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
void (*init)(void);
void (**ctors)(void);
size_t ctors_size;
2016-12-25 15:47:01 +03:00
void (**init_array)(void);
size_t init_array_size;
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
uintptr_t base;
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
list_t * dependencies;
2016-11-21 13:17:54 +03:00
int loaded;
2016-12-01 17:24:35 +03:00
} elf_t;
2016-12-12 13:27:07 +03:00
static elf_t * _main_obj = NULL;
static char * find_lib(const char * file) {
2016-12-02 18:16:12 +03:00
if (strchr(file, '/')) return strdup(file);
char * path = getenv("LD_LIBRARY_PATH");
if (!path) {
path = "/usr/lib:/lib:/opt/lib";
}
char * xpath = strdup(path);
int found = 0;
char * p, * tokens[10], * last;
int i = 0;
for ((p = strtok_r(xpath, ":", &last)); p; p = strtok_r(NULL, ":", &last)) {
int r;
struct stat stat_buf;
char * exe = malloc(strlen(p) + strlen(file) + 2);
strcpy(exe, p);
strcat(exe, "/");
strcat(exe, file);
r = stat(exe, &stat_buf);
if (r != 0) {
free(exe);
continue;
}
return exe;
}
free(xpath);
return NULL;
}
2016-12-01 17:24:35 +03:00
static elf_t * open_object(const char * path) {
2016-12-12 13:27:07 +03:00
if (!path) {
_main_obj->loaded = 1;
return _main_obj;
}
if (hashmap_has(objects_map, (void*)path)) {
elf_t * object = hashmap_get(objects_map, (void*)path);
object->loaded = 1;
return object;
}
2016-12-02 18:16:12 +03:00
char * file = find_lib(path);
if (!file) return NULL;
FILE * f = fopen(file, "r");
free(file);
2016-12-01 17:24:35 +03:00
if (!f) {
return NULL;
2016-11-21 13:17:54 +03:00
}
2016-12-01 17:24:35 +03:00
elf_t * object = calloc(1, sizeof(elf_t));
hashmap_set(objects_map, (void*)path, object);
2016-12-01 17:24:35 +03:00
if (!object) {
return NULL;
2016-11-21 13:17:54 +03:00
}
2016-12-01 17:24:35 +03:00
object->file = f;
size_t r = fread(&object->header, sizeof(Elf32_Header), 1, object->file);
if (!r) {
free(object);
return NULL;
2016-11-21 13:17:54 +03:00
}
2016-12-01 17:24:35 +03:00
if (object->header.e_ident[0] != ELFMAG0 ||
object->header.e_ident[1] != ELFMAG1 ||
object->header.e_ident[2] != ELFMAG2 ||
object->header.e_ident[3] != ELFMAG3) {
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
free(object);
return NULL;
}
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
object->dependencies = list_create();
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
return object;
}
2016-12-01 17:24:35 +03:00
static size_t object_calculate_size(elf_t * object) {
2016-12-01 17:24:35 +03:00
uintptr_t base_addr = 0xFFFFFFFF;
uintptr_t end_addr = 0x0;
{
size_t headers = 0;
2016-12-01 17:24:35 +03:00
while (headers < object->header.e_phnum) {
Elf32_Phdr phdr;
2016-12-01 17:24:35 +03:00
fseek(object->file, object->header.e_phoff + object->header.e_phentsize * headers, SEEK_SET);
fread(&phdr, object->header.e_phentsize, 1, object->file);
switch (phdr.p_type) {
case PT_LOAD:
{
2016-12-01 17:24:35 +03:00
if (phdr.p_vaddr < base_addr) {
base_addr = phdr.p_vaddr;
}
if (phdr.p_memsz + phdr.p_vaddr > end_addr) {
end_addr = phdr.p_memsz + phdr.p_vaddr;
}
2016-12-01 17:24:35 +03:00
}
break;
default:
break;
}
headers++;
}
}
if (base_addr == 0xFFFFFFFF) return 0;
return end_addr - base_addr;
}
static uintptr_t object_load(elf_t * object, uintptr_t base) {
uintptr_t end_addr = 0x0;
object->base = base;
/* Load object */
{
size_t headers = 0;
while (headers < object->header.e_phnum) {
Elf32_Phdr phdr;
fseek(object->file, object->header.e_phoff + object->header.e_phentsize * headers, SEEK_SET);
fread(&phdr, object->header.e_phentsize, 1, object->file);
switch (phdr.p_type) {
case PT_LOAD:
{
char * args[] = {(char *)(base + phdr.p_vaddr), (char *)phdr.p_memsz};
syscall_system_function(10, args);
fseek(object->file, phdr.p_offset, SEEK_SET);
fread((void *)(base + phdr.p_vaddr), phdr.p_filesz, 1, object->file);
size_t r = phdr.p_filesz;
while (r < phdr.p_memsz) {
*(char *)(phdr.p_vaddr + base + r) = 0;
r++;
}
2016-12-01 17:24:35 +03:00
if (end_addr < phdr.p_vaddr + base + phdr.p_memsz) {
end_addr = phdr.p_vaddr + base + phdr.p_memsz;
}
}
break;
case PT_DYNAMIC:
2016-12-01 17:24:35 +03:00
{
object->dynamic = (Elf32_Dyn *)(base + phdr.p_vaddr);
}
break;
default:
break;
}
headers++;
}
}
2016-11-21 13:17:54 +03:00
2016-12-01 17:24:35 +03:00
return end_addr;
}
static int object_postload(elf_t * object) {
/* Load section string table */
2016-11-29 15:30:15 +03:00
{
2016-12-01 17:24:35 +03:00
Elf32_Shdr shdr;
fseek(object->file, object->header.e_shoff + object->header.e_shentsize * object->header.e_shstrndx, SEEK_SET);
fread(&shdr, object->header.e_shentsize, 1, object->file);
object->string_table = malloc(shdr.sh_size);
fseek(object->file, shdr.sh_offset, SEEK_SET);
fread(object->string_table, shdr.sh_size, 1, object->file);
}
if (object->dynamic) {
Elf32_Dyn * table;
/* Locate string table */
table = object->dynamic;
while (table->d_tag) {
switch (table->d_tag) {
case 4:
object->dyn_hash = (Elf32_Word *)(object->base + table->d_un.d_ptr);
object->dyn_symbol_table_size = object->dyn_hash[1];
break;
case 5: /* Dynamic String Table */
object->dyn_string_table = (char *)(object->base + table->d_un.d_ptr);
break;
case 6: /* Dynamic Symbol Table */
object->dyn_symbol_table = (Elf32_Sym *)(object->base + table->d_un.d_ptr);
break;
case 10: /* Size of string table */
object->dyn_string_table_size = table->d_un.d_val;
break;
case 12:
object->init = (void (*)(void))(table->d_un.d_ptr + object->base);
break;
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
table++;
}
table = object->dynamic;
while (table->d_tag) {
switch (table->d_tag) {
case 1:
list_insert(object->dependencies, object->dyn_string_table + table->d_un.d_val);
break;
}
table++;
2016-11-29 15:30:15 +03:00
}
}
size_t i = 0;
for (uintptr_t x = 0; x < object->header.e_shentsize * object->header.e_shnum; x += object->header.e_shentsize) {
Elf32_Shdr shdr;
fseek(object->file, object->header.e_shoff + x, SEEK_SET);
fread(&shdr, object->header.e_shentsize, 1, object->file);
if (!strcmp((char *)((uintptr_t)object->string_table + shdr.sh_name), ".ctors")) {
object->ctors = (void *)(shdr.sh_addr + object->base);
object->ctors_size = shdr.sh_size / sizeof(uintptr_t);
}
2016-12-25 15:47:01 +03:00
if (!strcmp((char *)((uintptr_t)object->string_table + shdr.sh_name), ".init_array")) {
object->init_array = (void *)(shdr.sh_addr + object->base);
object->init_array_size = shdr.sh_size / sizeof(uintptr_t);
2016-12-25 15:47:01 +03:00
}
}
2016-12-01 17:24:35 +03:00
return 0;
}
static int need_symbol_for_type(unsigned char type) {
switch(type) {
case 1:
2016-12-01 18:15:37 +03:00
case 2:
2016-12-01 17:24:35 +03:00
case 5:
case 6:
case 7:
return 1;
default:
return 0;
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
}
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
static int object_relocate(elf_t * object) {
if (object->dyn_symbol_table) {
Elf32_Sym * table = object->dyn_symbol_table;
2016-11-29 15:30:15 +03:00
size_t i = 0;
2016-12-01 17:24:35 +03:00
while (i < object->dyn_symbol_table_size) {
char * symname = (char *)((uintptr_t)object->dyn_string_table + table->st_name);
if (!hashmap_has(dumb_symbol_table, symname)) {
if (table->st_shndx) {
hashmap_set(dumb_symbol_table, symname, (void*)(table->st_value + object->base));
}
} else {
if (table->st_shndx) {
//table->st_value = (uintptr_t)hashmap_get(dumb_symbol_table, symname);
2016-11-29 15:30:15 +03:00
}
}
2016-12-01 17:24:35 +03:00
table++;
2016-11-29 15:30:15 +03:00
i++;
}
}
2016-12-01 17:24:35 +03:00
size_t i = 0;
for (uintptr_t x = 0; x < object->header.e_shentsize * object->header.e_shnum; x += object->header.e_shentsize) {
Elf32_Shdr shdr;
fseek(object->file, object->header.e_shoff + x, SEEK_SET);
fread(&shdr, object->header.e_shentsize, 1, object->file);
if (shdr.sh_type == 9) {
Elf32_Rel * table = (Elf32_Rel *)(shdr.sh_addr + object->base);
while ((uintptr_t)table - ((uintptr_t)shdr.sh_addr + object->base) < shdr.sh_size) {
unsigned int symbol = ELF32_R_SYM(table->r_info);
unsigned char type = ELF32_R_TYPE(table->r_info);
Elf32_Sym * sym = &object->dyn_symbol_table[symbol];
char * symname = NULL;
2016-12-01 17:24:35 +03:00
uintptr_t x = sym->st_value + object->base;
if (need_symbol_for_type(type) || (type == 5)) {
2016-12-01 17:24:35 +03:00
symname = (char *)((uintptr_t)object->dyn_string_table + sym->st_name);
}
if ((sym->st_shndx == 0) && need_symbol_for_type(type) || (type == 5)) {
2016-12-12 13:27:07 +03:00
if (symname && hashmap_has(dumb_symbol_table, symname)) {
2016-12-01 17:24:35 +03:00
x = (uintptr_t)hashmap_get(dumb_symbol_table, symname);
} else {
TRACE_LD("Symbol not found: %s", symname);
2016-12-01 17:24:35 +03:00
x = 0x0;
2016-11-29 15:30:15 +03:00
}
}
2016-12-01 17:24:35 +03:00
/* Relocations, symbol lookups, etc. */
switch (type) {
case 6: /* GLOB_DAT */
2016-12-12 13:27:07 +03:00
if (symname && hashmap_has(glob_dat, symname)) {
2016-12-01 17:24:35 +03:00
x = (uintptr_t)hashmap_get(glob_dat, symname);
}
case 7: /* JUMP_SLOT */
memcpy((void *)(table->r_offset + object->base), &x, sizeof(uintptr_t));
break;
case 1: /* 32 */
x += *((ssize_t *)(table->r_offset + object->base));
memcpy((void *)(table->r_offset + object->base), &x, sizeof(uintptr_t));
break;
case 2: /* PC32 */
x += *((ssize_t *)(table->r_offset + object->base));
x -= (table->r_offset + object->base);
memcpy((void *)(table->r_offset + object->base), &x, sizeof(uintptr_t));
break;
case 8: /* RELATIVE */
x = object->base;
x += *((ssize_t *)(table->r_offset + object->base));
memcpy((void *)(table->r_offset + object->base), &x, sizeof(uintptr_t));
break;
case 5: /* COPY */
memcpy((void *)(table->r_offset + object->base), (void *)x, sym->st_size);
break;
default:
TRACE_LD("Unknown relocation type: %d", type);
}
table++;
2016-11-29 15:30:15 +03:00
}
}
}
2016-12-01 17:24:35 +03:00
return 0;
}
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
static void object_find_copy_relocations(elf_t * object) {
size_t i = 0;
for (uintptr_t x = 0; x < object->header.e_shentsize * object->header.e_shnum; x += object->header.e_shentsize) {
Elf32_Shdr shdr;
fseek(object->file, object->header.e_shoff + x, SEEK_SET);
fread(&shdr, object->header.e_shentsize, 1, object->file);
if (shdr.sh_type == 9) {
Elf32_Rel * table = (Elf32_Rel *)(shdr.sh_addr + object->base);
while ((uintptr_t)table - ((uintptr_t)shdr.sh_addr + object->base) < shdr.sh_size) {
unsigned char type = ELF32_R_TYPE(table->r_info);
if (type == 5) {
unsigned int symbol = ELF32_R_SYM(table->r_info);
Elf32_Sym * sym = &object->dyn_symbol_table[symbol];
char * symname = (char *)((uintptr_t)object->dyn_string_table + sym->st_name);
hashmap_set(glob_dat, symname, (void *)table->r_offset);
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
table++;
2016-11-29 15:30:15 +03:00
}
}
}
2016-12-01 17:24:35 +03:00
}
2016-12-05 12:07:20 +03:00
static char * last_error = NULL;
2016-12-01 17:24:35 +03:00
static void * object_find_symbol(elf_t * object, const char * symbol_name) {
2016-12-05 12:07:20 +03:00
if (!object->dyn_symbol_table) {
last_error = "lib does not have a symbol table";
return NULL;
}
2016-12-01 17:24:35 +03:00
Elf32_Sym * table = object->dyn_symbol_table;
size_t i = 0;
while (i < object->dyn_symbol_table_size) {
if (!strcmp(symbol_name, (char *)((uintptr_t)object->dyn_string_table + table->st_name))) {
return (void *)(table->st_value + object->base);
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
table++;
i++;
2016-11-29 15:30:15 +03:00
}
2016-12-05 12:07:20 +03:00
last_error = "symbol not found in library";
2016-12-01 17:24:35 +03:00
return NULL;
}
static void * do_actual_load(const char * filename, elf_t * lib, int flags) {
(void)flags;
2016-12-05 11:40:10 +03:00
2016-12-05 12:07:20 +03:00
if (!lib) {
last_error = "could not open library (not found, or other failure)";
return NULL;
}
2016-12-05 11:40:10 +03:00
size_t lib_size = object_calculate_size(lib);
if (lib_size < 4096) {
lib_size = 4096;
}
uintptr_t load_addr = (uintptr_t)malloc(lib_size);
object_load(lib, load_addr);
object_postload(lib);
node_t * item;
while (item = list_pop(lib->dependencies)) {
elf_t * lib = open_object(item->value);
if (!lib) {
free((void *)load_addr);
last_error = "Failed to load a dependency.";
return NULL;
}
if (!lib->loaded) {
do_actual_load(item->value, lib, 0);
2016-12-17 15:24:25 +03:00
TRACE_LD("Loaded %s at 0x%x", item->value, lib->base);
}
}
2016-12-05 11:40:10 +03:00
TRACE_LD("Relocating %s", filename);
object_relocate(lib);
fclose(lib->file);
if (lib->ctors) {
for (size_t i = 0; i < lib->ctors_size; i++) {
2016-12-05 11:40:10 +03:00
TRACE_LD(" 0x%x()", lib->ctors[i]);
lib->ctors[i]();
}
}
2016-12-25 15:47:01 +03:00
if (lib->init_array) {
for (size_t i = 0; i < lib->init_array_size; i++) {
2016-12-25 15:47:01 +03:00
TRACE_LD(" 0x%x()", lib->init_array[i]);
lib->init_array[i]();
}
}
2016-12-05 11:40:10 +03:00
if (lib->init) {
lib->init();
}
return (void *)lib;
}
static void * dlopen_ld(const char * filename, int flags) {
TRACE_LD("dlopen(%s,0x%x)", filename, flags);
elf_t * lib = open_object(filename);
2016-12-12 13:27:07 +03:00
if (lib->loaded) {
return lib;
}
2016-12-17 15:24:25 +03:00
void * ret = do_actual_load(filename, lib, flags);
TRACE_LD("Loaded %s at 0x%x", filename, lib->base);
return ret;
2016-12-05 11:40:10 +03:00
}
static int dlclose_ld(elf_t * lib) {
/* TODO close dependencies? Make sure nothing references this. */
free((void *)lib->base);
return 0;
}
static char * dlerror_ld(void) {
/* TODO actually do this */
2016-12-05 12:07:20 +03:00
char * this_error = last_error;
last_error = NULL;
return this_error;
2016-12-05 11:40:10 +03:00
}
typedef struct {
char * name;
2016-12-01 17:24:35 +03:00
void * symbol;
2016-12-05 11:40:10 +03:00
} ld_exports_t;
ld_exports_t ld_builtin_exports[] = {
{"dlopen", dlopen_ld},
{"dlsym", object_find_symbol},
{"dlclose", dlclose_ld},
{"dlerror", dlerror_ld},
{NULL, NULL},
2016-12-01 17:24:35 +03:00
};
int main(int argc, char * argv[]) {
2016-12-02 18:16:12 +03:00
char * file = argv[1];
size_t arg_offset = 1;
if (!strcmp(argv[1], "-e")) {
arg_offset = 3;
file = argv[2];
}
2016-12-01 17:24:35 +03:00
char * trace_ld_env = getenv("LD_DEBUG");
if (trace_ld_env && (!strcmp(trace_ld_env,"1") || !strcmp(trace_ld_env,"yes"))) {
__trace_ld = 1;
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
dumb_symbol_table = hashmap_create(10);
glob_dat = hashmap_create(10);
objects_map = hashmap_create(10);
2016-12-01 17:24:35 +03:00
2016-12-05 11:40:10 +03:00
ld_exports_t * ex = ld_builtin_exports;
while (ex->name) {
hashmap_set(dumb_symbol_table, ex->name, ex->symbol);
ex++;
}
2016-12-02 18:16:12 +03:00
elf_t * main_obj = open_object(file);
2016-12-12 13:27:07 +03:00
_main_obj = main_obj;
2016-11-29 15:30:15 +03:00
2016-12-01 17:24:35 +03:00
if (!main_obj) {
2016-12-02 18:16:12 +03:00
fprintf(stderr, "%s: error: failed to open object '%s'.\n", argv[0], file);
2016-12-01 17:24:35 +03:00
return 1;
}
size_t main_size = object_calculate_size(main_obj);
uintptr_t end_addr = object_load(main_obj, 0x0);
object_postload(main_obj);
object_find_copy_relocations(main_obj);
hashmap_t * libs = hashmap_create(10);
2016-12-25 15:47:01 +03:00
while (end_addr & 0xFFF) {
end_addr++;
}
list_t * ctor_libs = list_create();
list_t * init_libs = list_create();
2016-12-01 17:24:35 +03:00
TRACE_LD("Loading dependencies.");
node_t * item;
while (item = list_pop(main_obj->dependencies)) {
while (end_addr & 0xFFF) {
end_addr++;
}
char * lib_name = item->value;
if (!strcmp(lib_name, "libg.so")) goto nope;
elf_t * lib = open_object(lib_name);
if (!lib) {
fprintf(stderr, "Failed to load dependency '%s'.\n", lib_name);
return 1;
2016-11-29 15:30:15 +03:00
}
2016-12-01 17:24:35 +03:00
hashmap_set(libs, lib_name, lib);
TRACE_LD("Loading %s at 0x%x", lib_name, end_addr);
end_addr = object_load(lib, end_addr);
object_postload(lib);
TRACE_LD("Relocating %s", lib_name);
object_relocate(lib);
fclose(lib->file);
/* Execute constructors */
2016-12-25 15:47:01 +03:00
if (lib->ctors || lib->init_array) {
list_insert(ctor_libs, lib);
}
if (lib->init) {
list_insert(init_libs, lib);
}
2016-12-01 17:24:35 +03:00
nope:
free(item);
}
TRACE_LD("Relocating main object");
object_relocate(main_obj);
TRACE_LD("Placing heap at end");
while (end_addr & 0xFFF) {
end_addr++;
2016-11-29 15:30:15 +03:00
}
char * ld_no_ctors = getenv("LD_DISABLE_CTORS");
if (ld_no_ctors && (!strcmp(ld_no_ctors,"1") || !strcmp(ld_no_ctors,"yes"))) {
TRACE_LD("skipping ctors because LD_DISABLE_CTORS was set");
} else {
foreach(node, ctor_libs) {
elf_t * lib = node->value;
if (lib->ctors) {
TRACE_LD("Executing ctors...");
for (size_t i = 0; i < lib->ctors_size; i++) {
TRACE_LD(" 0x%x()", lib->ctors[i]);
lib->ctors[i]();
}
}
2016-12-25 15:47:01 +03:00
if (lib->init_array) {
TRACE_LD("Executing init_array...");
for (size_t i = 0; i < lib->init_array_size; i++) {
2016-12-25 15:47:01 +03:00
TRACE_LD(" 0x%x()", lib->init_array[i]);
lib->init_array[i]();
}
}
}
}
foreach(node, init_libs) {
elf_t * lib = node->value;
lib->init();
}
if (main_obj->init_array) {
for (size_t i = 0; i < main_obj->init_array_size; i++) {
TRACE_LD(" 0x%x()", main_obj->init_array[i]);
main_obj->init_array[i]();
}
}
if (main_obj->init) {
main_obj->init();
}
{
2016-12-01 17:24:35 +03:00
char * args[] = {(char*)end_addr};
syscall_system_function(9, args);
}
2016-12-01 17:24:35 +03:00
TRACE_LD("Jumping to entry point");
entry_point_t entry = (entry_point_t)main_obj->header.e_entry;
2016-12-02 18:16:12 +03:00
entry(argc-arg_offset,argv+arg_offset,environ);
2016-11-21 13:17:54 +03:00
return 0;
}