是否可以在不包含标准库的情况下将字符串输出到 C 中的控制台?

Is it possible to output a string to the console in C without including the standard library?

我正在努力更好地理解汇编代码和机器代码的工作原理。 所以我正在用 gcc 编译这个简单的片段:

#include <stdio.h>
int main(){
    printf("Hello World!");
    return 0;
}

但这包括默认库。我想在不使用 printf 的情况下输出 hello world,而是通过在 C 文件中内联一些程序集,并向 gcc 添加 -nostdlib 和 -nodefaultlibs 选项。我怎样才能做到这一点 ?我正在使用 Windows 10 和 mingw-w64 以及英特尔酷睿 i7 6700 HQ(笔记本电脑处理器)。我可以在 windows 上使用 NASM 和 gcc 吗?

您可以在 NASM 32 位 linux 上执行此操作,方法是将字符串移动到内存中写入 STDOUT 文件并调用 SYS_WRITE。

在 windows 上,这样做比较复杂,而且没有什么有用的学习经验,因此我建议您设置 WSL 或 linux 虚拟机并按照以下步骤操作。

有关如何操作的教程,请参阅以下链接:
32 位(WSL 不支持):
https://asmtutor.com/#lesson1
64 位:
http://briansteffens.com/introduction-to-64-bit-assembly/01-hello-world/

Link 用于设置 WSL:
https://docs.microsoft.com/en-us/windows/wsl/install-win10

我建议不要使用 GCC 的内联汇编。很难做到正确。您问 我可以在 windows 上将 NASM 与 GCC 一起使用吗?。答案是YES,请做!您可以 link 您的 64 位 NASM 代码到 Win64 对象,然后 link 它与您的 C 程序。

您必须了解 Win64 API。与 Linux 不同,您不应该直接进行系统调用。您调用 Windows API ,它是系统调用接口的薄包装。

为了写入控制台,使用 Console API you need to use a function like GetStdHandle to get a handle to STDOUT and then call a function like WriteConsoleA 将 ANSI 字符串写入控制台。

编写汇编代码时,您必须了解调用约定。 Win64 calling convention is documented by Microsoft. It is also described in this Wiki article。来自 Microsoft 文档的摘要:

Calling convention defaults

The x64 Application Binary Interface (ABI) uses a four-register fast-call calling convention by default. Space is allocated on the call stack as a shadow store for callees to save those registers. There's a strict one-to-one correspondence between the arguments to a function call and the registers used for those arguments. Any argument that doesn’t fit in 8 bytes, or isn't 1, 2, 4, or 8 bytes, must be passed by reference. A single argument is never spread across multiple registers. The x87 register stack is unused, and may be used by the callee, but must be considered volatile across function calls. All floating point operations are done using the 16 XMM registers. Integer arguments are passed in registers RCX, RDX, R8, and R9. Floating point arguments are passed in XMM0L, XMM1L, XMM2L, and XMM3L. 16-byte arguments are passed by reference. Parameter passing is described in detail in Parameter Passing. In addition to these registers, RAX, R10, R11, XMM4, and XMM5 are considered volatile. All other registers are non-volatile.

My note: the shadow store is 32 bytes that have to be allocated on the stack after any stack arguments before a C or Win64 API function call is made.

这是一个 NASM 程序,它调用函数 WriteString 函数,该函数将要打印的字符串作为第一个参数,将字符串的长度作为第二个参数。 WinMain 是 Windows 控制台程序的默认入口点:

global WinMain                  ; Make the default console entry point globally visible
global WriteString              ; Make function WriteString globally visible          

default rel                     ; Default to RIP relative addressing rather
                                ;     than absolute

; External Win API functions available in kernel32
extern WriteConsoleA
extern GetStdHandle
extern ExitProcess

SHADOW_AREA_SIZE  EQU 32
STD_OUTPUT_HANDLE EQU -11

; Read Only Data section
section .rdata use64
strBrownFox db "The quick brown fox jumps over the lazy dog!"
strBrownFox_len equ $-strBrownFox

; Data section (read/write)
section .data use64

; BSS section (read/write) zero-initialized
section .bss use64
numCharsWritten: resd 1      ; reserve space for one 4-byte dword

; Code section
section .text use64

; Default Windows entry point in 64-bit code
WinMain:
    push rsp                 ; Align stack on 16-byte boundary. 8 bytes were
                             ;     pushed by the CALL that reached us. 8+8=16

    lea rcx, [strBrownFox]   ; Parameter 1 = address of string to print
    mov edx, strBrownFox_len ; Parameter 2 = length of string to print
    call WriteString

    xor ecx, ecx             ; Exit and return 0
    call ExitProcess

WriteString:
    push rbp
    mov rbp, rsp             ; Creating a stack frame is optional
    push rdi                 ; Non volatile register we clobber that has to be saved
    push rsi                 ; Non volatile register we clobber that has to be saved
    sub rsp, 16+SHADOW_AREA_SIZE
                             ; The number of bytes pushed must be a multiple of 8
                             ;     to maintain alignment. That includes RBP, the registers
                             ;     we save and restore, the maximum number of extra
                             ;     parameters needed by all the WinAPI calls we make
                             ;     And the Shadow Area Size. 8+8+8+16+32=72.
                             ;     72 is multiple of 8 so at this point our stack
                             ;     is aligned on a 16 byte boundary. 8 bytes were pushed
                             ;     by the call to reach WriteString.
                             ;     72+8=80 = 80 is evenly divisible by 16 so stack remains
                             ;     properly aligned after the SUB instruction

    mov rdi, rcx             ; Store string address to RDI (Parameter 1 = RCX)
    mov esi, edx             ; Store string length to RSI (Parameter 2 = RDX)

    ; HANDLE WINAPI GetStdHandle(
    ;  _In_ DWORD nStdHandle
    ; );
    mov ecx, STD_OUTPUT_HANDLE
    call GetStdHandle

    ; BOOL WINAPI WriteConsole(
    ;  _In_             HANDLE  hConsoleOutput,
    ;  _In_       const VOID    *lpBuffer,
    ;  _In_             DWORD   nNumberOfCharsToWrite,
    ;  _Out_            LPDWORD lpNumberOfCharsWritten,
    ;  _Reserved_       LPVOID  lpReserved
    ; );

    mov ecx, eax             ; RCX = File Handle for STDOUT.
                             ; GetStdHandle returned handle in EAX

    mov rdx, rdi             ; RDX = address of string to display
    mov r8d, esi             ; R8D = length of string to display       
    lea r9, [numCharsWritten]
    mov qword [rsp+SHADOW_AREA_SIZE+0], 0
                             ; 5th parameter passed on the stack above
                             ;     the 32 byte shadow space. Reserved needs to be 0 
    call WriteConsoleA

    pop rsi                  ; Restore the non volatile registers we clobbered 
    pop rdi
    mov rsp, rbp
    pop rbp
    ret

您可以 assemble 和 link 使用这些命令:

nasm -f win64 myprog.asm -o myprog.obj
gcc -nostartfiles -nostdlib -nodefaultlibs myprog.obj -lkernel32 -lgcc -o myprog.exe

当你 运行 myprog.exe 它应该显示:

The quick brown fox jumps over the lazy dog!

您还可以将 C 文件编译成目标文件,然后将它们 link 编译成此代码并从汇编中调用它们。在这个例子中,GCC 只是被用作 linker.


编译 C 文件并用汇编代码链接

这个例子类似于第一个例子,除了我们创建一个名为 cfuncs.cC 文件,它调用我们的汇编语言 WriteString 函数来打印 你好,世界!:

cfuncs.c

/* WriteString is the assembly language function to write to console*/
extern void WriteString (const char *str, int len);

/* Implement strlen */
size_t strlen(const char *str)
{
    const char *s = str;
    for (; *s; ++s)
        ;

    return (s-str);
}

void PrintHelloWorld(void)
{
    char *strHelloWorld = "Hello, world!\n";
    WriteString (strHelloWorld, strlen(strHelloWorld));
    return;
}

myprog.asm

default rel                     ; Default to RIP relative addressing rather
                                ;     than absolute

global WinMain                  ; Make the default console entry point globally visible
global WriteString              ; Make function WriteString globally visible          

; Our own external C functions from our .c file
extern PrintHelloWorld

; External Win API functions in kernel32
extern WriteConsoleA
extern GetStdHandle
extern ExitProcess

SHADOW_AREA_SIZE  EQU 32    
STD_OUTPUT_HANDLE EQU -11

; Read Only Data section
section .rdata use64
strBrownFox db "The quick brown fox jumps over the lazy dog!", 13, 10
strBrownFox_len equ $-strBrownFox

; Data section (read/write)
section .data use64

; BSS section (read/write) zero-initialized
section .bss use64
numCharsWritten: resd 1      ; reserve space for one 4-byte dword

; Code section
section .text use64

; Default Windows entry point in 64-bit code
WinMain:
    push rsp                 ; Align stack on 16-byte boundary. 8 bytes were
                             ;     pushed by the CALL that reached us. 8+8=16

    lea rcx, [strBrownFox]   ; Parameter 1 = address of string to print
    mov edx, strBrownFox_len ; Parameter 2 = length of string to print
    call WriteString

    call PrintHelloWorld     ; Call C function that prints Hello, world!

    xor ecx, ecx             ; Exit and return 0
    call ExitProcess

WriteString:
    push rbp
    mov rbp, rsp             ; Creating a stack frame is optional
    push rdi                 ; Non volatile register we clobber that has to be saved
    push rsi                 ; Non volatile register we clobber that has to be saved
    sub rsp, 16+SHADOW_AREA_SIZE
                             ; The number of bytes pushed must be a multiple of 8
                             ;     to maintain alignment. That includes RBP, the registers
                             ;     we save and restore, the maximum number of extra
                             ;     parameters needed by all the WinAPI calls we make
                             ;     And the Shadow Area Size. 8+8+8+16+32=72.
                             ;     72 is multiple of 8 so at this point our stack
                             ;     is aligned on a 16 byte boundary. 8 bytes were pushed
                             ;     by the call to reach WriteString.
                             ;     72+8=80 = 80 is evenly divisible by 16 so stack remains
                             ;     properly aligned after the SUB instruction

    mov rdi, rcx             ; Store string address to RDI (Parameter 1 = RCX)
    mov esi, edx             ; Store string length to RSI (Parameter 2 = RDX)

    ; HANDLE WINAPI GetStdHandle(
    ;  _In_ DWORD nStdHandle
    ; );
    mov ecx, STD_OUTPUT_HANDLE
    call GetStdHandle

    ; BOOL WINAPI WriteConsole(
    ;  _In_             HANDLE  hConsoleOutput,
    ;  _In_       const VOID    *lpBuffer,
    ;  _In_             DWORD   nNumberOfCharsToWrite,
    ;  _Out_            LPDWORD lpNumberOfCharsWritten,
    ;  _Reserved_       LPVOID  lpReserved
    ; );

    mov ecx, eax             ; RCX = File Handle for STDOUT.
                             ; GetStdHandle returned handle in EAX

    mov rdx, rdi             ; RDX = address of string to display
    mov r8d, esi             ; R8D = length of string to display       
    lea r9, [numCharsWritten]
    mov qword [rsp+SHADOW_AREA_SIZE+0], 0
                             ; 5th parameter passed on the stack above
                             ;     the 32 byte shadow space. Reserved needs to be 0 
    call WriteConsoleA

    pop rsi                  ; Restore the non volatile registers we clobbered 
    pop rdi
    mov rsp, rbp
    pop rbp
    ret

要 assemble、编译和 link 为可执行文件,您可以使用这些命令:

nasm -f win64 myprog.asm -o myprog.obj
gcc -c cfuncs.c -o cfuncs.obj
gcc -nodefaultlibs -nostdlib -nostartfiles myprog.obj cfuncs.obj -lkernel32 -lgcc -o myprog.exe 

myprog.exe 的输出应该是:

The quick brown fox jumps over the lazy dog!
Hello, world!