是否可以在不包含标准库的情况下将字符串输出到 C 中的控制台? [英] Is it possible to output a string to the console in C without including the standard library?

查看:70
本文介绍了是否可以在不包含标准库的情况下将字符串输出到 C 中的控制台?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图更好地理解汇编和机器代码的工作原理.所以我正在用 gcc 编译这个简单的 snipet:

#include int main(){printf("你好世界!");返回0;}

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

解决方案

我建议不要使用 GCC 的内联程序集.很难做到正确.您问我可以在 Windows 上将 NASM 与 GCC 一起使用吗?.答案是YES,拜托了!您可以将 64 位 NASM 代码链接到 Win64 对象,然后将其链接到您的 C 程序.

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

为了使用 控制台写入控制台API 你需要使用像 GetStdHandle 获取 STDOUT 的句柄,然后调用类似 的函数WriteConsoleA 将 ANSI 字符串写入控制台.

在编写汇编代码时,您必须了解调用约定.Win64 调用约定是由微软记录.此维基文章中也对此进行了描述.Microsoft 文档中的摘要:

<块引用>

调用约定默认值

默认情况下,x64 应用程序二进制接口 (ABI) 使用四寄存器快速调用调用约定.在调用堆栈上分配空间作为被调用者保存这些寄存器的影子存储.函数调用的参数和用于这些参数的寄存器之间存在严格的一一对应关系.任何不适合 8 个字节或不是 1、2、4 或 8 个字节的参数都必须通过引用传递.单个参数永远不会分布在多个寄存器中.x87 寄存器堆栈未使用,可以由被调用方使用,但必须在函数调用之间被视为易失性.所有浮点运算均使用 16 个 XMM 寄存器完成.整数参数在寄存器 RCX、RDX、R8 和 R9 中传递.浮点参数在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递.16 字节参数通过引用传递.参数传递在参数传递中有详细描述.除了这些寄存器之外,RAX、R10、R11、XMM4 和 XMM5 被认为是易失性的.所有其他寄存器都是非易失性的.

我的注意:影子存储是 32 个字节,必须在 C 或 Win64 之前的任何堆栈参数之后在堆栈上分配API函数调用完成.

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

全局 WinMain ;使默认控制台入口点全局可见全局写入字符串;使函数 WriteString 全局可见默认 rel ;默认为 RIP 相对寻址;比绝对;kernel32 中可用的外部 Win API 函数外部写控制台A外部获取标准句柄外部退出进程SHADOW_AREA_SIZE 等于 32STD_OUTPUT_HANDLE EQU -11;只读数据部分.rdata 节 use64strBrownFox db 敏捷的棕色狐狸跳过懒狗!"strBrownFox_len equ $-strBrownFox;数据部分(读/写)节 .data use64;BSS 部分(读/写)零初始化.bss 节 use64numCharsWritten: resd 1 ;为一个 4 字节的双字保留空间;代码部分节 .text use64;64 位代码中的默认 Windows 入口点WinMain:推 rsp ;在 16 字节边界上对齐堆栈.8个字节是;由到达我们的 CALL 推动.8+8=16lea rcx, [strBrownFox] ;参数 1 = 要打印的字符串地址mov edx, strBrownFox_len ;参数 2 = 要打印的字符串长度调用 WriteString异或 ecx, ecx ;退出并返回 0调用 ExitProcess写字符串:推RBP移动 rbp, rsp ;创建堆栈帧是可选的推 rdi ;我们破坏了必须保存的非易失性寄存器推 rsi ;我们破坏了必须保存的非易失性寄存器sub rsp, 16+SHADOW_AREA_SIZE;推送的字节数必须是 8 的倍数;以保持对齐.这包括 RBP,寄存器;我们保存和恢复,最大数量的额外;我们进行的所有 WinAPI 调用所需的参数;和阴影区域大小.8+8+8+16+32=72.;72 是 8 的倍数,所以此时我们的堆栈;在 16 字节边界上对齐.推送了 8 个字节;通过调用到达WriteString.;72+8=80 = 80 可以被 16 整除,所以堆栈仍然存在;在 SUB 指令后正确对齐mov rdi, rcx ;将字符串地址存储到 RDI(参数 1 = RCX)mov esi, edx ;将字符串长度存储到 RSI(参数 2 = RDX);处理 WINAPI GetStdHandle(;_In_ DWORD nStdHandle;);mov ecx, STD_OUTPUT_HANDLE调用 GetStdHandle;BOOL WINAPI 写控制台(;_In_ 处理 hConsoleOutput,;_In_ const VOID *lpBuffer,;_In_ DWORD nNumberOfCharsToWrite,;_Out_ LPDWORD lpNumberOfCharsWritten,;_Reserved_ LPVOID lpReserved;);mov ecx, eax ;RCX = STDOUT 的文件句柄.;GetStdHandle 返回 EAX 中的句柄移动 rdx, rdi ;RDX = 要显示的字符串地址mov r8d, esi ;R8D = 要显示的字符串长度lea r9, [numCharsWritten]mov qword [rsp+SHADOW_AREA_SIZE+0], 0;在上面的堆栈上传递的第 5 个参数;32 字节的影子空间.保留需要为 0调用 WriteConsoleA流行 rsi ;恢复我们破坏的非易失性寄存器流行音乐mov rsp, rbp流行音乐回复

您可以使用以下命令进行组装和链接:

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

当您运行 myprog.exe 时,它应该显示:

<块引用>

敏捷的棕色狐狸跳过懒狗!

您还可以将 C 文件编译为目标文件并将它们链接到此代码并从程序集中调用它们.在此示例中,GCC 仅用作链接器.

<小时>

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

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

cfuncs.c

/* WriteString 是写入控制台的汇编语言函数*/extern void WriteString (const char *str, int len);/* 实现 strlen */size_t strlen(const char *str){const char *s = str;对于 (; *s; ++s);返回(s-str);}无效打印HelloWorld(无效){char *strHelloWorld = "你好,世界!\n";WriteString (strHelloWorld, strlen(strHelloWorld));返回;}

myprog.asm

默认 rel ;默认为 RIP 相对寻址;比绝对全局 WinMain ;使默认控制台入口点全局可见全局写入字符串;使函数 WriteString 全局可见;我们自己的外部 C 函数来自我们的 .c 文件外部打印HelloWorld;kernel32 中的外部 Win API 函数外部写控制台A外部获取标准句柄外部退出进程SHADOW_AREA_SIZE 等于 32STD_OUTPUT_HANDLE EQU -11;只读数据部分.rdata 节 use64strBrownFox db 敏捷的棕色狐狸跳过懒狗!", 13, 10strBrownFox_len equ $-strBrownFox;数据部分(读/写)节 .data use64;BSS 部分(读/写)零初始化.bss 节 use64numCharsWritten: resd 1 ;为一个 4 字节的双字保留空间;代码部分节 .text use64;64 位代码中的默认 Windows 入口点WinMain:推 rsp ;在 16 字节边界上对齐堆栈.8个字节是;由到达我们的 CALL 推动.8+8=16lea rcx, [strBrownFox] ;参数 1 = 要打印的字符串地址mov edx, strBrownFox_len ;参数 2 = 要打印的字符串长度调用 WriteString调用 PrintHelloWorld ;调用打印 Hello, world! 的 C 函数.异或 ecx, ecx ;退出并返回 0调用 ExitProcess写字符串:推RBP移动 rbp, rsp ;创建堆栈帧是可选的推 rdi ;我们破坏了必须保存的非易失性寄存器推 rsi ;我们破坏了必须保存的非易失性寄存器sub rsp, 16+SHADOW_AREA_SIZE;推送的字节数必须是 8 的倍数;以保持对齐.这包括 RBP,寄存器;我们保存和恢复,最大数量的额外;我们进行的所有 WinAPI 调用所需的参数;和阴影区域大小.8+8+8+16+32=72.;72 是 8 的倍数,所以此时我们的堆栈;在 16 字节边界上对齐.推送了 8 个字节;通过调用到达WriteString.;72+8=80 = 80 可以被 16 整除,所以堆栈仍然存在;在 SUB 指令后正确对齐mov rdi, rcx ;将字符串地址存储到 RDI(参数 1 = RCX)mov esi, edx ;将字符串长度存储到 RSI(参数 2 = RDX);处理 WINAPI GetStdHandle(;_In_ DWORD nStdHandle;);mov ecx, STD_OUTPUT_HANDLE调用 GetStdHandle;BOOL WINAPI 写控制台(;_In_ 处理 hConsoleOutput,;_In_ const VOID *lpBuffer,;_In_ DWORD nNumberOfCharsToWrite,;_Out_ LPDWORD lpNumberOfCharsWritten,;_Reserved_ LPVOID lpReserved;);mov ecx, eax ;RCX = STDOUT 的文件句柄.;GetStdHandle 返回 EAX 中的句柄移动 rdx, rdi ;RDX = 要显示的字符串地址mov r8d, esi ;R8D = 要显示的字符串长度lea r9, [numCharsWritten]mov qword [rsp+SHADOW_AREA_SIZE+0], 0;在上面的堆栈上传递的第 5 个参数;32 字节的影子空间.保留需要为 0调用 WriteConsoleA流行 rsi ;恢复我们破坏的非易失性寄存器流行音乐mov rsp, rbp流行音乐回复

要汇编、编译和链接到可执行文件,您可以使用以下命令:

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

myprog.exe 的输出应该是:

<块引用>

敏捷的棕色狐狸跳过懒狗!你好,世界!

I'm trying to get better understanding of how assembly and machine code works. So I'm compiling this simple snipet with gcc :

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

But this includes the default library. I would like to output hello world without using printf but by inlining some assembly in the C file, and adding -nostdlib and -nodefaultlibs options to gcc. How can I do that ? I'm using Windows 10 and mingw-w64 with Intel core i7 6700 HQ (laptop processor). Can I use NASM with gcc on windows ?

解决方案

I recommend against using GCC's inline assembly. It is hard to get right. You ask the question Can I use NASM with GCC on windows?. The answer is YES, please do! You can link your 64-bit NASM code to a Win64 object and then link it with your C program.

You have to have knowledge of the Win64 API. Unlike Linux you aren't suppose to make system calls directly. You call the Windows API which is a thin wrapper around the system call interface.

For the purposes of writing to the console using the Console API you need to use a function like GetStdHandle to get a handle to STDOUT and then call a function like WriteConsoleA to write an ANSI string to the console.

When writing assembly code you have to have knowledge of the calling convention. Win64 calling convention is documented by Microsoft. It is also described in this Wiki article. A summary from the Microsoft documentation:

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.

This is a NASM program that calls a function WriteString function that takes a string to print as the first parameter and the length of the string for the second. WinMain is the default entry point for Windows console programs:

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

You can assemble, and link with these commands:

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

When you run myprog.exe it should display:

The quick brown fox jumps over the lazy dog!

You can also compile C files into object files and link them to this code and call them from assembly as well. In this example GCC is simply being used as a linker.


Compiling C Files and Linking with Assembly Code

This example is similar to the first one except we create a C file called cfuncs.c that calls our assembly language WriteString function to print Hello, world!:

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

To assemble, compile, and link to an executable you can use these commands:

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 

The output of myprog.exe should be:

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

这篇关于是否可以在不包含标准库的情况下将字符串输出到 C 中的控制台?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆