📜 ⬆️ ⬇️

Embedding into the Linux kernel: intercepting system calls

The term "system call" in programming and computing refers to the application program to the core of the operating system (OS) to perform any operation. Since this interaction is the main one, interception of system calls seems to be the most important stage of embedding, since allows you to control a key component of the OS kernel — the system call interface, which, in turn, makes it possible to inspect application requests for kernel services.

This article is a continuation of the previously announced cycle , dedicated to the particular issues of the implementation of the imposed remedies, and, in particular, embedding in software systems.


')

I. Approaches to embedding



There are various ways to intercept system calls to the Linux kernel. First of all, it is worth noting that the method of interception of kernel functions discussed earlier can be used to intercept single system calls. Indeed, due to the fact that most of the system calls are represented by the corresponding functions (for example, sys_open ), the task of intercepting them is equivalent to the task of intercepting these functions. However, with an increase in the number of intercepted system calls and the increasing complexity of "business logic", this approach may be limited.

A more universal way is to modify the entries in the system call tables (more details on the tables will be described later) containing pointers to functions that implement the logic of a particular system call. The tables are used by the kernel during dispatching, when according to the number of the system call requested by the application, a pointer to the handler function is selected from the corresponding table with its subsequent execution. Replacing such an index will change the logic of the kernel in handling system calls. Looking ahead, it is worth noting that for the successful implementation of this method, the tables themselves will need to be somehow found, since they are not exported. Ultimately, intercepting the system call will consist in a simple redefinition of the table element.

The most universal method of intercepting system calls was and remains modifying the code of the system call manager so that the pre- and post-processing of the context of the thread requesting any system service is provided. This option gives greater flexibility in comparison with the previous ones, since introduces a single point of state control before and after the handler function.

Further, an example will be described in detail how to embed the Linux kernel into the system call interface, modifying the dispatchers code.

Ii. System call dispatch in the Linux kernel



Dispatching system calls is a rather complicated process with a lot of nuances, however, many details will be omitted in this article, because, except for the dispatching process itself (fetching and executing a function corresponding to a system call), you don’t need to know anything to implement embedding .

Traditionally, the Linux kernel supports the following system call features for the x86 architecture:



Below is an excellent illustration of the implementation of a system call, borrowed by me, depending on the variant used:

image

As you can see, 32-bit applications make system calls using INT 80h and SYSENTER, whereas 64-bit applications use SYSCALL. At the same time, there is support for the ability to execute 32-bit code in a 64-bit environment (the so-called compatibility mode is emulation / compatibility mode; the kernel option is CONFIG_IA32_EMULATION ). In this regard, there are 2 non-exportable tables in the kernel - sys_call_table and ia32_sys_call_table (available only for emulation mode), containing the addresses of functions that process system calls.

In the general case, when all possible mechanisms are presented in the 64-bit kernel, there are 4 entry points that define what the logic of the corresponding dispatcher will be:



One way or another, when an application makes a system call, the kernel gets control. The system call manager for each of the cases examined has differences from the others, but without loss of generality, their overall structure can be examined using the example system_call :

  0xffffffff81731670 <+0>: swapgs 0xffffffff81731673 <+3>: mov %rsp,%gs:0xc000 0xffffffff8173167c <+12>: mov %gs:0xc830,%rsp 0xffffffff81731685 <+21>: sti 0xffffffff81731686 <+22>: data32 data32 xchg %ax,%ax 0xffffffff8173168a <+26>: data32 xchg %ax,%ax 0xffffffff8173168d <+29>: sub $0x50,%rsp 0xffffffff81731691 <+33>: mov %rdi,0x40(%rsp) 0xffffffff81731696 <+38>: mov %rsi,0x38(%rsp) 0xffffffff8173169b <+43>: mov %rdx,0x30(%rsp) 0xffffffff817316a0 <+48>: mov %rax,0x20(%rsp) 0xffffffff817316a5 <+53>: mov %r8,0x18(%rsp) 0xffffffff817316aa <+58>: mov %r9,0x10(%rsp) 0xffffffff817316af <+63>: mov %r10,0x8(%rsp) 0xffffffff817316b4 <+68>: mov %r11,(%rsp) 0xffffffff817316b8 <+72>: mov %rax,0x48(%rsp) 0xffffffff817316bd <+77>: mov %rcx,0x50(%rsp) 0xffffffff817316c2 <+82>: testl $0x100801d1,-0x1f78(%rsp) 0xffffffff817316cd <+93>: jne 0xffffffff8173181e <tracesys> 0xffffffff817316d3 <+0>: and $0xbfffffff,%eax 0xffffffff817316d8 <+5>: cmp $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */ 0xffffffff817316dd <+10>: ja 0xffffffff817317a5 <badsys> 0xffffffff817316e3 <+16>: mov %r10,%rcx 0xffffffff817316e6 <+19>: callq *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */ 0xffffffff817316ed <+26>: mov %rax,0x20(%rsp) 0xffffffff817316f2 <+0>: mov $0x1008feff,%edi 0xffffffff817316f7 <+0>: cli 0xffffffff817316f8 <+1>: data32 data32 xchg %ax,%ax 0xffffffff817316fc <+5>: data32 xchg %ax,%ax 0xffffffff817316ff <+8>: mov -0x1f78(%rsp),%edx 0xffffffff81731706 <+15>: and %edi,%edx 0xffffffff81731708 <+17>: jne 0xffffffff81731745 <sysret_careful> 0xffffffff8173170a <+19>: mov 0x50(%rsp),%rcx 0xffffffff8173170f <+24>: mov (%rsp),%r11 0xffffffff81731713 <+28>: mov 0x8(%rsp),%r10 0xffffffff81731718 <+33>: mov 0x10(%rsp),%r9 0xffffffff8173171d <+38>: mov 0x18(%rsp),%r8 0xffffffff81731722 <+43>: mov 0x20(%rsp),%rax 0xffffffff81731727 <+48>: mov 0x30(%rsp),%rdx 0xffffffff8173172c <+53>: mov 0x38(%rsp),%rsi 0xffffffff81731731 <+58>: mov 0x40(%rsp),%rdi 0xffffffff81731736 <+63>: mov %gs:0xc000,%rsp 0xffffffff8173173f <+72>: swapgs 0xffffffff81731742 <+75>: sysretq 


As you can see, the first instruction ( swapgs ) switches data structures (from user to nuclear). Next, the stack is configured, interrupts are enabled, and the register context of the flow ( pt_regs structure) required during processing is formed on the stack. Returning to the listing presented above, special attention should be paid to the following commands:

  0xffffffff817316d8 <+5>: cmp $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */ 0xffffffff817316dd <+10>: ja 0xffffffff817317a5 <badsys> 0xffffffff817316e3 <+16>: mov %r10,%rcx 0xffffffff817316e6 <+19>: callq *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */ 0xffffffff817316ed <+26>: mov %rax,0x20(%rsp) 


The first line checks whether the number of the requested system call (register %rax ) %rax maximum allowable value ( __NR_syscall_max ). In the event that the check is successful, the system call will be dispatched, namely, the control will transfer to a function that implements the appropriate logic.

Thus, a key point in the processing of system calls is the dispatch command, which is a function call *sys_call_table(,%rax,8) ( call *sys_call_table(,%rax,8) ). Further embedding will be carried out by modifying this command.

Iii. Embedding technique



As noted, a universal way of embedding in the dispatcher will be modifying its code in such a way as to ensure that the context of the flow can be controlled before it performs the function of implementing the system call logic (pre-processing), as well as after it has been executed (post-processing).

In order to implement embedding in this way, it is proposed to slightly patch the dispatcher by modifying the dispatch command ( call *sys_call_table(,%rax,8) ), and write an unconditional branch command ( JMP REL32 ) on it to the service_stub handler. In this case, the general structure of such a handler will be as follows (hereinafter pseudocode):

 system_call: swapgs .. jmp service_stub /* <--------   call *sys_call_table(,%rax,8) */ mov %rax,0x20(%rsp) /* <--------     service_stub */ ... swapgs sysretq service_stub: ... call ServiceTraceEnter /* void ServiceTraceEnter(struct pt_regs *) */ ... call sys_call_table[N](args) ... call ServiceTraceLeave(regs) /* void ServiceTraceLeave(struct pt_regs *) */ ... jmp back 


Here, ServiceTraceEnter() and ServiceTraceLeave() are the pre- and post-processing functions, respectively. Their parameters are a pointer to a pt_regs - a register structure representing the stream context. The final instruction is the command to transfer control to the system call manager code, from where the call to this handler was previously made.

Below is the service_syscall64 handler code used as an example for system_call interception (SYSCALL instruction):

 .global service_syscall64 service_syscall64: SAVE_REST movq %rsp, %rdi call ServiceTraceEnter RESTORE_REST LOAD_ARGS 0 movq %r10, %rcx movq ORIG_RAX - ARGOFFSET(%rsp), %rax call *0x00000000(,%rax,8) // origin call movq %rax, RAX - ARGOFFSET(%rsp) SAVE_REST movq %rsp, %rdi call ServiceTraceLeave RESTORE_REST movq RAX - ARGOFFSET(%rsp), %rax jmp 0x00000000 


As can be seen, it has the structure considered above. The exact values ​​of pointers and offsets are adjusted during the module loading process (this will be described below). In addition, the above fragment contains additional elements ( SAVE_REST , RESTORE_REST , LOAD_ARGS ), the purpose of which is mainly to form a stream context ( pt_regs ) before calling the ServiceTraceEnter and ServiceTraceLeave .

Iv. Features of embedding



The implementation of embedding into the dispatching mechanisms of the system calls of the Linux kernel somehow implies the need to solve the following practical tasks:



Determining System Call Manager Addresses

The presence of several dispatchers in the system suggests the need to determine their addresses. It was noted above that each dispatcher corresponds to his “method” of making a request for a system call. Therefore, appropriate mechanisms will be used to determine the required addresses:



Thus, each of the desired addresses is easily determined.

Determining the addresses of the system call dispatch tables

As noted above, the sys_call_table and ia32_sys_call_table not exported. There are different ways to determine their addresses, but by defining the addresses of dispatchers in the previous step, the addresses of the tables are also determined simply by searching for a dispatching instruction, which has the form call sys_call_table[N] .

For these purposes it is rational to use the disassembler ( udis86 ). By successively searching through the instructions, starting with the very first, you can reach the command you are looking for, whose argument is the address of the corresponding table. Due to the fact that the structure of dispatchers is well-established, it is possible to unambiguously determine the characteristics of the command being searched for (CALL with a length of 7 bytes) and with a high degree of reliability obtain from it the required value of the address of the table.

If for some reason this is not enough, you can enhance the verification of the received address. To do this, for example, you can check whether the value in the cell with the number __NR_open expected table is equal to the address of the sys_open function. However, in this example, such additional checks are not performed.

Dispatcher code modification

When modifying the code of system call managers, it is necessary to take into account the fact that their code is read-only (ReadOnly). In addition, code modification on the working system should be carried out atomically, i.e. so that during the modification process there are no undefined states when any of the threads sees a partially completed record.

In a previous article, the correct way to write to write-protected pages using the creation of temporary mappings is discussed. There is no need to repeat something here. As for atomicity, this issue was also covered earlier , when the topic of interception of nuclear functions was considered.

Thus, it is advisable to modify the write-protected code using temporary mappings , as well as a special interface of the Linux kernel - stop_machine .

Setting handlers

In accordance with the introduced embedding method, the code of each of the dispatchers is modified so that the 7-byte CALL MEM32 dispatch CALL MEM32 is replaced with the 5-byte unconditional branch command with the corresponding JMP REL32 handler. As a result, certain restrictions are imposed on the distance of the transition. The handler must be located no further than ± 2 GB from the location of the JMP REL32 .

In accordance with the structure of the handlers, they contain commands (JMP and CALL) that require the specification of precise arguments (for example, return addresses or system call table addresses). Due to the fact that such values ​​are not available at the stage of compilation or module loading, they must be put down “manually”, after loading, before starting work.

Another important feature when setting up handlers is the need to ensure that the module can be unloaded while maintaining system health. For these purposes, the handler code must remain in the system even after the main module is unloaded (more on this later).

Unloading module

Unloading of the module should be carried out with preservation of system performance. This means that after the module has been unloaded, the system should function normally. This task is not trivial due to the fact that with unloading the module, all the code used in it is unloaded.

For example, one can imagine a situation that some thread performing a system call “fell asleep” in the core. Until the moment when he wakes up, someone is trying to unload the module. In principle, nothing prevents to make this system. As a result, when the thread in question wakes up and completes the requested system call, the control will return to the appropriate handler (which is why it should not be unloaded).

However, the non-uploading of the handlers code is not the only condition for the preservation of the system operability after unloading the module. It is worth recalling that the real system call in the handler was wrapped in a couple of calls to the ServiceTraceEnter and ServiceTraceLeave trace functions, the code of which was located in the unloaded module.

Therefore, in order not to get into a situation where, on returning from a system call, the thread would try to call a function that is no longer physically needed, it is necessary to re-modify the code of each handler, eliminating invalid more calls from there (in other words, by filling them with NOPs).

V. Features of the implementation of the kernel module



The following describes the structure of the kernel module, which is embedded in the dispatching system of system calls of the Linux kernel.

The key structure of the module is a struct scentry - a structure containing the information necessary for embedding into the corresponding dispatcher. The structure contains the following fields:

 typedef struct scentry { const char *name; const void *entry; const void *table; const void *pcall; void *pcall_map; void *stub; const void *handler; void (*prepare)(struct scentry *); void (*implant)(struct scentry *); void (*restore)(struct scentry *); void (*cleanup)(struct scentry *); } scentry_t; 


Structures are combined into an array that defines how and with what parameters to embed:

 scentry_t elist[] = { ... { .name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (1) */ .handler = service_syscall64, .prepare = prepare_syscall64_1 }, { .name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (2) */ .handler = service_syscall64, .prepare = prepare_syscall64_2 }, ... }; 


With the exception of the indicated fields, the filling of the remaining elements of the structure occurs automatically — the prepare function is responsible for this. Below is an example of the implementation of the function to prepare for embedding a SYSCALL command in the manager:

 extern void service_syscall64(void); static void prepare_syscall64_1(scentry_t *se) { /* * searching for -- 'call *sys_call_table(,%rax,8)' * http://lxr.free-electrons.com/source/arch/x86/kernel/entry_64.S?v=3.13#L629 */ se->entry = get_symbol_address(se->name); se->entry = se->entry ? se->entry : to_ptr(x86_get_msr(MSR_LSTAR)); if (!se->entry) return; se->pcall = ud_find_insn(se->entry, 512, UD_Icall, 7); if (!se->pcall) return; se->table = to_ptr(*(int *)(se->pcall + 3)); } 


As you can see, the first attempt is to resolve the name of the character to its corresponding address ( se->entry ). If it is not possible to determine the address in this way, the mechanisms specific to each dispatcher come into play (in this case, reading the MSR register with the number MSR_LSTAR).

Further, for the found dispatcher, the dispatch command ( se->pcall ) is se->pcall and, if successful, the address used by the system call table manager is determined.

The completion of the preparation phase is the creation of a handler code used by the dispatcher after it has been modified. Below is the stub_fixup function that does this:

 static void fixup_stub(scentry_t *se) { ud_t ud; memset(se->stub, 0x90, STUB_SIZE); ud_initialize(&ud, BITS_PER_LONG, \ UD_VENDOR_ANY, se->handler, STUB_SIZE); while (ud_disassemble(&ud)) { void *insn = se->stub + ud_insn_off(&ud); const void *orig_insn = se->handler + ud_insn_off(&ud); memcpy(insn, orig_insn, ud_insn_len(&ud)); /* fixup sys_call_table dispatcher calls (FF.14.x5.xx.xx.xx.xx) */ if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 7) { x86_insert_call(insn, NULL, se->table, 7); continue; } /* fixup ServiceTraceEnter/Leave calls (E8.xx.xx.xx.xx) */ if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) { x86_insert_call(insn, insn, orig_insn + (long)(*(int *)(orig_insn + 1)) + 5, 5); continue; } /* fixup jump back (E9.xx.xx.xx.xx) */ if (ud.mnemonic == UD_Ijmp && ud_insn_len(&ud) == 5) { x86_insert_jmp(insn, insn, se->pcall + 7); break; } } se->pcall_map = map_writable(se->pcall, 64); } 


As you can see, the main role of this function is to create a copy of handlers and then adjust them to actual addresses. Here is also actively used disassembler. - . JMP REL32 , .

. ( JMP REL32 ) .

restore , , :

 static void generic_restore(scentry_t *se) { ud_t ud; if (!se->pcall_map) return; ud_initialize(&ud, BITS_PER_LONG, \ UD_VENDOR_ANY, se->stub, STUB_SIZE); while (ud_disassemble(&ud)) { if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) { memset(se->stub + ud_insn_off(&ud), 0x90, ud_insn_len(&ud)); continue; } if (ud.mnemonic == UD_Ijmp) break; } debug(" [o] restoring original call instruction %p (%s)\n", se->pcall, se->name); x86_insert_call(se->pcall_map, NULL, se->table, 7); } 


, 5- CALL NOP', . .

implant restore stop_machine , .

clenup , ( pcall_map ).

, , . , .

Thus, using the example, the basic principles of embedding the system kernel calls into the mechanism are analyzed, and the possibility of intercepting them is illustrated.

Vi. Testing and debugging



For testing purposes, intercept the system call open(2). Below is the trace_syscall_entry function that implements this interception using the ServiceTraceEnter handler :

 static void trace_syscall_entry(int arch, unsigned long major, \ unsigned long a0, unsigned long a1, unsigned long a2, unsigned long a3) { char *filename = NULL; if (major == __NR_open || major == __NR_ia32_open) { filename = kmalloc(PATH_MAX, GFP_KERNEL); if (!filename || strncpy_from_user(filename, (const void __user *)a0, PATH_MAX) < 0) goto out; printk("%s open(%s) [%s]\n", arch ? "X86_64" : "I386", filename, current->comm); } out: if (filename) kfree(filename); } void ServiceTraceEnter(struct pt_regs *regs) { if (IS_IA32) trace_syscall_entry(0, regs->orig_ax, \ regs->bx, regs->cx, regs->dx, regs->si); #ifdef CONFIG_X86_64 else trace_syscall_entry(1, regs->orig_ax, \ regs->di, regs->si, regs->dx, regs->r10); #endif } 


The module is assembled and loaded by standard means:

 $ git clone https://github.com/milabs/kmod_hooking_sct $ cd kmod_hooking_sct $ make $ sudo insmod scthook.ko 


As a result dmesg, the following information should appear in the kernel log (command ):

 [ 5217.779766] [scthook] # SYSCALL hooking module [ 5217.780132] [scthook] # prepare [ 5217.785853] [scthook] [o] prepared stub ffffffffa000c000 (ia32_syscall) [ 5217.785856] [scthook] entry:ffffffff81731e30 pcall:ffffffff81731e92 table:ffffffff81809cc0 [ 5217.790482] [scthook] [o] prepared stub ffffffffa000c200 (ia32_sysenter_target) [ 5217.790484] [scthook] entry:ffffffff817319a0 pcall:ffffffff81731a36 table:ffffffff81809cc0 [ 5217.794931] [scthook] [o] prepared stub ffffffffa000c400 (ia32_cstar_target) [ 5217.794933] [scthook] entry:ffffffff81731be0 pcall:ffffffff81731c75 table:ffffffff81809cc0 [ 5217.797517] [scthook] [o] prepared stub ffffffffa000c600 (system_call) [ 5217.797518] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172fd26 table:ffffffff81801400 [ 5217.800013] [scthook] [o] prepared stub ffffffffa000c800 (system_call) [ 5217.800014] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172ff38 table:ffffffff81801400 [ 5217.800014] [scthook] # prepare OK [ 5217.800015] [scthook] # implant [ 5217.800052] [scthook] [o] implanting jump to stub handler ffffffffa000c000 (ia32_syscall) [ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c200 (ia32_sysenter_target) [ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c400 (ia32_cstar_target) [ 5217.800055] [scthook] [o] implanting jump to stub handler ffffffffa000c600 (system_call) [ 5217.800056] [scthook] [o] implanting jump to stub handler ffffffffa000c800 (system_call) [ 5217.800058] [scthook] # implant OK 


Proper interception processing open(2)will result in the following messages in the same log:

 [ 5370.999929] X86_64 open(/usr/share/locale-langpack/en_US.utf8/LC_MESSAGES/libc.mo) [perl] [ 5370.999930] X86_64 open(/usr/share/locale-langpack/en_US/LC_MESSAGES/libc.mo) [perl] [ 5370.999932] X86_64 open(/usr/share/locale-langpack/en.UTF-8/LC_MESSAGES/libc.mo) [perl] [ 5370.999934] X86_64 open(/usr/share/locale-langpack/en.utf8/LC_MESSAGES/libc.mo) [perl] [ 5370.999936] X86_64 open(/usr/share/locale-langpack/en/LC_MESSAGES/libc.mo) [perl] [ 5371.001308] X86_64 open(/etc/login.defs) [cron] [ 5372.422399] X86_64 open(/home/ilya/.cache/awesome/history) [awesome] [ 5372.424013] X86_64 open(/dev/null) [awesome] [ 5372.424682] I386 open(/etc/ld.so.cache) [skype] [ 5372.424714] I386 open(/usr/lib/i386-linux-gnu/libXv.so.1) [skype] [ 5372.424753] I386 open(/usr/lib/i386-linux-gnu/libXss.so.1) [skype] [ 5372.424789] I386 open(/lib/i386-linux-gnu/librt.so.1) [skype] [ 5372.424827] I386 open(/lib/i386-linux-gnu/libdl.so.2) [skype] [ 5372.424856] I386 open(/usr/lib/i386-linux-gnu/libX11.so.6) [skype] [ 5372.424896] I386 open(/usr/lib/i386-linux-gnu/libXext.so.6) [skype] [ 5372.424929] I386 open(/usr/lib/i386-linux-gnu/libQtDBus.so.4) [skype] [ 5372.424961] I386 open(/usr/lib/i386-linux-gnu/libQtWebKit.so.4) [skype] [ 5372.425003] I386 open(/usr/lib/i386-linux-gnu/libQtXml.so.4) [skype] [ 5372.425035] I386 open(/usr/lib/i386-linux-gnu/libQtGui.so.4) [skype] [ 5372.425072] I386 open(/usr/lib/i386-linux-gnu/libQtNetwork.so.4) [skype] [ 5372.425103] I386 open(/usr/lib/i386-linux-gnu/libQtCore.so.4) [skype] [ 5372.425151] I386 open(/lib/i386-linux-gnu/libpthread.so.0) [skype] [ 5372.425191] I386 open(/usr/lib/i386-linux-gnu/libstdc++.so.6) [skype] [ 5372.425233] I386 open(/lib/i386-linux-gnu/libm.so.6) [skype] [ 5372.425265] I386 open(/lib/i386-linux-gnu/libgcc_s.so.1) [skype] [ 5372.425292] I386 open(/lib/i386-linux-gnu/libc.so.6) [skype] [ 5372.425338] I386 open(/usr/lib/i386-linux-gnu/libxcb.so.1) [skype] [ 5372.425380] I386 open(/lib/i386-linux-gnu/libdbus-1.so.3) [skype] [ 5372.425416] I386 open(/lib/i386-linux-gnu/libz.so.1) [skype] [ 5372.425444] I386 open(/usr/lib/i386-linux-gnu/libXrender.so.1) [skype] [ 5372.425475] I386 open(/usr/lib/i386-linux-gnu/libjpeg.so.8) [skype] [ 5372.425510] I386 open(/lib/i386-linux-gnu/libpng12.so.0) [skype] [ 5372.425546] I386 open(/usr/lib/i386-linux-gnu/libxslt.so.1) [skype] [ 5372.425579] I386 open(/usr/lib/i386-linux-gnu/libxml2.so.2) [skype] 


, 32- (, Skype) , , I386, X86_64. , open(2) .

VII. Conclusion



Linux , . , , , . .

, , github .

Source: https://habr.com/ru/post/245539/


All Articles