어셈블리없는 C / C ++ 함수 정의
나는 항상 printf()
마지막 단계에서 같은 함수 가 인라인 어셈블리를 사용하여 정의 되었다고 생각했습니다 . stdio.h의 내부 깊숙한 곳에는 실제로 CPU에 무엇을해야하는지 알려주는 asm 코드가 묻혀 있습니다. 예를 들어, dos에서는 mov
문자열의 시작 부분을 메모리 위치 나 레지스터에 int
먼저 호출하고 terupt를 호출하는 방식 으로 구현 된 것을 기억합니다 .
그러나 x64 버전의 Visual Studio는 인라인 어셈블러를 전혀 지원하지 않기 때문에 C / C ++에서 어셈블러 정의 함수가 전혀 없을 수있는 방법이 궁금했습니다. printf()
어셈블러 코드를 사용하지 않고 C / C ++로 구현되는 것과 같은 라이브러리 기능은 어떻게 됩니까? 실제로 올바른 소프트웨어 인터럽트를 실행하는 것은 무엇입니까? 감사.
어셈블러 코드를 사용하지 않고 printf ()와 같은 라이브러리 함수를 C / C ++로 구현하는 방법은 무엇입니까? 실제로 올바른 소프트웨어 인터럽트를 실행하는 것은 무엇입니까?
대부분의 실용적인 목적을 위해 Linux 또는 Windows에서 BIOS 를 호출 할 수 없습니다 . 그리고 실제로 운영 체제 나 부트 로더를 작성하지 않는 한 BIOS와 전혀 상호 작용하지 않아야합니다.
당신이 C 함수에 대해 구체적으로 묻고 있기 때문에 printf()
, 여기서 제공 할 것은 GNU의 libc에 대해 "고무가 도로를 만나는 곳" 을 찾기 위해 한 작은 추적 입니다. 스포일러 경고 : syscall () 에서 끝납니다 .
시스템 호출 은 BIOS가 아니라 OS가 기본 서비스를 수행하는 데 필요한 예상 매개 변수가있는 번호가 매겨진 함수 표입니다. "일부 매개 변수와 함께 합의 된 규칙 인 숫자로 무언가를 호출"한다는 의미에서 유사합니다. 이것이 모든 소프트웨어의 종류이지만, 차이점을 강조해야 합니다. 실제 모드의 하드웨어가 아니라 OS와 대화하는 것입니다.
그래서 여기에 GCC에 printf
대한 구체적 탐구가 있습니다 ... 쉽게 지루하지 않은 사람들을 위해 :
첫 단계
물론 파일에 정의되어있는 printf의 프로토 타입부터 시작하겠습니다. libc/libio/stdio.h
extern int printf (__const char *__restrict __format, ...);
그러나 printf라는 함수에 대한 소스 코드는 찾을 수 없습니다. 대신 파일에서 다음 /libc/stdio-common/printf.c
과 같은 함수와 관련된 약간의 코드를 찾을 수 있습니다 __printf
.
int __printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
동일한 파일의 매크로는이 함수가 밑줄이없는 printf의 별칭으로 정의되도록 연결을 설정합니다.
ldbl_strong_alias (__printf, printf);
printf는 stdout을 사용하여 vfprintf를 호출하는 얇은 레이어가 될 것입니다. 실제로 형식화 작업의 핵심은 vfprintf에서 수행되며 libc/stdio-common/vfprintf.c
. 꽤 긴 기능이지만 여전히 모두 C로되어 있음을 알 수 있습니다!
토끼 구멍 더 깊이…
vfprintf는 같은 파일에 정의 된 이상한 매크로 인 outchar와 outstring을 신비하게 호출합니다.
#define outchar(Ch) \
do \
{ \
register const INT_T outc = (Ch); \
if (PUTC (outc, s) == EOF || done == INT_MAX) \
{ \
done = -1; \
goto all_done; \
} \
++done; \
} \
while (0)
왜 그렇게 이상한지에 대한 질문을 피하면서 동일한 파일에서도 수수께끼의 PUTC에 의존한다는 것을 알 수 있습니다.
#define PUTC(C, F) IO_putwc_unlocked (C, F)
IO_putwc_unlocked
in 의 정의에 도달 libc/libio/libio.h
하면 더 이상 printf 작동 방식에 관심이 없다고 생각할 수 있습니다.
#define _IO_putwc_unlocked(_wch, _fp) \
(_IO_BE ((_fp)->_wide_data->_IO_write_ptr \
>= (_fp)->_wide_data->_IO_write_end, 0) \
? __woverflow (_fp, _wch) \
: (_IO_wint_t) (*(_fp)->_wide_data->_IO_write_ptr++ = (_wch)))
그러나 읽기가 조금 어렵지만 버퍼링 된 출력을 수행하고 있습니다. 파일 포인터의 버퍼에 충분한 공간이 있으면 문자를 그 안에 붙 __woverflow
입니다. 그렇지 않으면 . 버퍼가 부족할 때 유일한 옵션은 화면 (또는 파일 포인터가 나타내는 장치)으로 플러시하는 것이므로 마법 주문을 찾을 수 있기를 바랍니다.
C의 Vtables?
우리가 또 다른 실망스러운 수준의 간접적 인 방향을 뛰어 넘을 것이라고 생각했다면 당신이 옳을 것입니다. libc / libio / wgenops.c를 보면 다음과 같은 정의를 찾을 수 있습니다 __woverflow
.
wint_t
__woverflow (f, wch)
_IO_FILE *f;
wint_t wch;
{
if (f->_mode == 0)
_IO_fwide (f, 1);
return _IO_OVERFLOW (f, wch);
}
기본적으로 파일 포인터는 GNU 표준 라이브러리에서 객체로 구현됩니다. 데이터 멤버는 물론 JUMP 매크로의 변형으로 호출 할 수있는 함수 멤버도 있습니다. 파일 libc/libio/libioP.h
에서이 기술에 대한 약간의 문서를 찾을 수 있습니다.
/* THE JUMPTABLE FUNCTIONS.
* The _IO_FILE type is used to implement the FILE type in GNU libc,
* as well as the streambuf class in GNU iostreams for C++.
* These are all the same, just used differently.
* An _IO_FILE (or FILE) object is allows followed by a pointer to
* a jump table (of pointers to functions). The pointer is accessed
* with the _IO_JUMPS macro. The jump table has a eccentric format,
* so as to be compatible with the layout of a C++ virtual function table.
* (as implemented by g++). When a pointer to a streambuf object is
* coerced to an (_IO_FILE*), then _IO_JUMPS on the result just
* happens to point to the virtual function table of the streambuf.
* Thus the _IO_JUMPS function table used for C stdio/libio does
* double duty as the virtual function table for C++ streambuf.
*
* The entries in the _IO_JUMPS function table (and hence also the
* virtual functions of a streambuf) are described below.
* The first parameter of each function entry is the _IO_FILE/streambuf
* object being acted on (i.e. the 'this' parameter).
*/
따라서 IO_OVERFLOW
에서 찾을 때 파일 포인터 libc/libio/genops.c
에서 “1-parameter” __overflow
메서드 를 호출하는 매크로라는 것을 알 수 있습니다 .
#define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
다양한 파일 포인터 유형에 대한 점프 테이블은 libc / libio / fileops.c에 있습니다.
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, INTUSE(_IO_file_finish)),
JUMP_INIT(overflow, INTUSE(_IO_file_overflow)),
JUMP_INIT(underflow, INTUSE(_IO_file_underflow)),
JUMP_INIT(uflow, INTUSE(_IO_default_uflow)),
JUMP_INIT(pbackfail, INTUSE(_IO_default_pbackfail)),
JUMP_INIT(xsputn, INTUSE(_IO_file_xsputn)),
JUMP_INIT(xsgetn, INTUSE(_IO_file_xsgetn)),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, INTUSE(_IO_file_doallocate)),
JUMP_INIT(read, INTUSE(_IO_file_read)),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, INTUSE(_IO_file_seek)),
JUMP_INIT(close, INTUSE(_IO_file_close)),
JUMP_INIT(stat, INTUSE(_IO_file_stat)),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
_IO_new_file_overflow
와 _IO_file_overflow
동일한 #define도 있으며 전자는 동일한 소스 파일에 정의되어 있습니다. (참고 : INTUSE는 내부 사용을위한 함수를 표시하는 매크로 일 뿐이며 "이 함수는 인터럽트를 사용합니다"와 같은 것을 의미하지는 않습니다.)
우리는 아직있다?!
_IO_new_file_overflow의 소스 코드는 더 많은 버퍼 조작을 수행하지만 다음을 호출합니다 _IO_do_flush
.
#define _IO_do_flush(_f) \
INTUSE(_IO_do_write)(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base)
우리는 이제 _IO_do_write가 고무가 실제로 도로를 만나는 지점에 있습니다. 즉, 버퍼링되지 않은 실제 I / O 장치에 대한 직접 쓰기입니다. 적어도 우리는 희망 할 수 있습니다! 매크로에 의해 _IO_new_do_write에 매핑되며 다음과 같습니다.
static
_IO_size_t
new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
is not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data,
count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF+_IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
슬프게도 우리는 ... 다시 붙어있어 _IO_SYSWRITE
일을하고있다 :
/* The 'syswrite' hook is used to write data from an existing buffer
to an external file. It generalizes the Unix write(2) function.
It matches the streambuf::sys_write virtual function, which is
specific to this implementation. */
typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t);
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
그래서 do_write 내부에서 파일 포인터에 대한 write 메소드를 호출합니다. 위의 점프 테이블에서 _IO_new_file_write에 매핑 된 것을 알고 있습니다. 그러면 그게 무엇일까요?
_IO_ssize_t
_IO_new_file_write (f, data, n)
_IO_FILE *f;
const void *data;
_IO_ssize_t n;
{
_IO_ssize_t to_do = n;
while (to_do > 0)
{
_IO_ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? write_not_cancel (f->_fileno, data, to_do)
: write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
이제 그냥 쓰기를 호출합니다! 그 구현은 어디에 있습니까? 다음에서 글을 찾을 수 있습니다 libc/posix/unistd.h
.
/* Write N bytes of BUF to FD. Return the number written, or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;
(참고 : __wur
는__attribute__ ((__warn_unused_result__)))
테이블에서 생성 된 함수
그것은 단지 쓰기를위한 프로토 타입 일뿐입니다. GNU 표준 라이브러리에서 Linux 용 write.c 파일을 찾을 수 없습니다. 대신 libc / sysdeps / 디렉토리에서 다양한 방식으로 OS 쓰기 기능에 연결하는 플랫폼 별 방법을 찾을 수 있습니다.
우리는 Linux가 수행하는 방식을 계속 따라갈 것입니다. sysdeps/unix/syscalls.list
쓰기 기능을 자동으로 생성하는 데 사용되는 라는 파일이 있습니다 . 표의 관련 데이터는 다음과 같습니다.
File name: write
Caller: “-” (i.e. Not Applicable)
Syscall name: write
Args: Ci:ibn
Strong name: __libc_write
Weak names: __write, write
모든 신비 것을하지는 제외하고 Ci:ibn
. C는 "취소 가능"을 의미합니다. 콜론은 반환 유형과 인수 유형을 구분하며, 의미에 대한 자세한 설명을 원하면 코드를 생성하는 쉘 스크립트에서 주석을 볼 수 있습니다 libc/sysdeps/unix/make-syscalls.sh
.
So now we’re expecting to be able to link against a function called __libc_write which is generated by this shell script. But what’s being generated? Some C code which implements write via a macro called SYS_ify, which you’ll find in sysdeps/unix/sysdep.h
#define SYS_ify(syscall_name) __NR_##syscall_name
Ah, good old token-pasting :P. So basically, the implementation of this __libc_write
becomes nothing more than a proxy invocation of the syscall function with a parameter named __NR_write
, and the other arguments.
Where The Sidewalk Ends…
I know this has been a fascinating journey, but now we’re at the end of GNU libc. That number __NR_write
is defined by Linux. For 32-bit X86 architectures it will get you to linux/arch/x86/include/asm/unistd_32.h
:
#define __NR_write 4
The only thing left to look at, then, is the implementation of syscall. Which I may do at some point, but for now I’ll just point you over to some references for how to add a system call to Linux.
First, you have to understand the concept of rings.
A kernel runs in ring 0, meaning it has a full access to memory and opcodes.
A program runs usually in ring 3. It has a limited access to memory, and cannot use all the opcodes.
So when a software need more privileges (for opening a file, writing to a file, allocating memory, etc), it needs to asks the kernel.
This can be done in many ways. Software interrupts, SYSENTER, etc.
Let's take the example of software interrupts, with the printf() function:
1 - Your software calls printf().
2 - printf() processes your string, and args, and then needs to execute a kernel function, as writing to a file can't be done in ring 3.
3 - printf() generates a software interrupt, placing in a register the number of a kernel function (in that case, the write() function).
4 - The software execution is interrupted, and the instruction pointer moves to the kernel code. So we are now in ring 0, in a kernel function.
5 - The kernel process the request, writing to the file (stdout is a file descriptor).
6 - When done, the kernel returns to the software's code, using the iret instruction.
7 - The software's code continues.
So functions of the C standard library can be implemented in C. All it has to do is to know how to call the kernel when it need more privileges.
In Linux, strace
utility allows you to see what system calls are made by a program. So, taking a program like this
int main(){ printf("x"); return 0; }
Say, you compile it as printx
, then strace printx
gives
execve("./printx", ["./printx"], [/* 49 vars */]) = 0 brk(0) = 0xb66000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=119796, ...}) = 0 mmap(NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1811128, ...}) = 0 mmap(NULL, 3925208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000 mprotect(0x7fa6dbcbb000, 2093056, PROT_NONE) = 0 mmap(0x7fa6dbeba000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000 mmap(0x7fa6dbec0000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000 close(3) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000 arch_prctl(ARCH_SET_FS, 0x7fa6dc0c5700) = 0 mprotect(0x7fa6dbeba000, 16384, PROT_READ) = 0 mprotect(0x600000, 4096, PROT_READ) = 0 mprotect(0x7fa6dc0e7000, 4096, PROT_READ) = 0 munmap(0x7fa6dc0c7000, 119796) = 0 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000 write(1, "x", 1x) = 1 exit_group(0) = ?
The rubber meets the road (sort off, see below) in the next to last call of the trace: write(1,"x",1x)
. At this point the control passes from user-land printx
to the Linux kernel which handles the rest. write()
is a wrapper function declared in unistd.h
extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;
Most system calls are wrapped in this way. The wrapper function, as its name suggests, is little more than a thin code layer that places the arguments in the correct registers and then executes a software interrupt 0x80. The kernel traps the interrupt and the rest is history. Or at least that's the way it used to work. Apparently, the overhead of interrupt trapping was quite high and, as an earlier post pointed out, modern CPU architectures introduced sysenter
assembly instruction, which accomplishes the same result at speed. This page System Calls has quite a nice summary of how system calls work.
I feel that you will probably be a bit disappointed with this answer, as was I. Clearly, in some sense, this is a false bottom as there are still quite a few things that have to happen between the call to write()
and the point at which the graphics card frame buffer is actually modified to make the letter "x" appear on your screen. Zooming in on the point of contact (to stay with the "rubber against the road" analogy) by diving into the kernel is sure to be educational if a time consuming endeavor. I am guessing you would have to travel through several layers of abstraction like buffered output streams, character devices, etc. Be sure to post the results should you decide to follow up on this:)
The standard library functions are implemented on an underlying platform library (e.g. UNIX API) and/or by direct system calls (that are still C functions). The system calls are (on platforms that I know of) internally implemented by a call to a function with inline asm that puts a system call number and parameters in CPU registers and triggers an interrupt that the kernel then processes.
There are also other ways of communicating with hardware besides syscalls, but these are usually unavailable or rather limited when running under a modern operating system, or at least enabling them requires some syscalls. A device may be memory mapped, so that writes to certain memory addresses (via regular pointers) control the device. I/O ports are also often used and depending the architecture these are accessed by special CPU opcodes or they, too, may be memory mapped to specific addresses.
Well, all C++ statements except the semicolon and comments end up becoming machine code that tells CPU what to do. You can write your own printf function without resorting to assembly. The only operations that must be written in assembly are input and output from ports, and things that enable and disable interrupts.
However, assembly is still used in system level programming for performance reasons. Even though inline assembly is not supported, there is nothing that prevents you from writing a separate module in assembly and linking it to your application.
In general, library function are precompiled and distribute ad object. Inline assembler is used only in particular situation for performance reasons, but it's the exception, not the rule. Actually, printf doesn't seems to me a good candidate to be inline-assembled. Insetad, functions like memcpy, or memcmp. Very low-level functions may be compiled by a native assembler (masm? gnu asm?), and distribute as object in a library.
The compiler generates the assembly from the C/C++ source code.
참고URL : https://stackoverflow.com/questions/2442966/c-c-function-definitions-without-assembly
'program tip' 카테고리의 다른 글
ASP.NET Core에서 IPrincipal 모의 (0) | 2020.10.29 |
---|---|
LINQ의 FirstOrDefault와 함께 KeyValuePair가 있는지 확인하십시오. (0) | 2020.10.29 |
ARC가 활성화 된 UUID 문자열 생성 (0) | 2020.10.29 |
두 목록을 JUnit 테스트하는 방법 (0) | 2020.10.29 |
객체에 특정 속성이 있는지 어떻게 테스트 할 수 있습니까? (0) | 2020.10.29 |