首页 > 解决方案 > 二进制数据作为命令行参数

问题描述

我有一个简单的 c++ 程序(和 c 的类似程序),它只打印出第一个参数

#include <iostream>

int main(int argc, char** argv)
{
    if(argc > 1)
        std::cout << ">>" << argv[1] << "<<\n";
}

我可以将二进制数据(我在 bash 上尝试过)作为参数传递,例如

$./a.out $(printf "1\x0123")
  >>1?23<<

如果我尝试传递一个空值,我会得到

./a.out $(printf "1\x0023")
bash: warning: command substitution: ignored null byte in input
>>123<<

显然 bash(?) 不允许这样做

但是是否可以通过这种方式将 null 作为命令行参数发送?c 或 c++ 是否对此有任何限制?

编辑:我没有在日常 C++ 中使用它,这个问题只是出于好奇

标签: c++ccommand-linearguments

解决方案


此答案是用 C 编写的,但可以编译为 C++ 并且在两者中的工作方式相同。我引用了 C11 标准;C++ 标准中有等价的定义。

没有将空字节传递给程序参数的好方法

C11 §5.1.2.2.1 程序启动
如果 的值argc大于零,则argv[0]通过argv[argc-1]inclusive 的数组成员应包含指向字符串的指针,这些指针在程序启动之前由主机环境给出实现定义的值。

C11 §7.1.1 术语定义
字符串是由第一个空字符终止并包括第一个空字符的连续字符序列。

这意味着传递给main()in的每个参数argv都是一个以 null 结尾的字符串。在字符串末尾的空字节之后没有可靠的数据——搜索那里会超出字符串的范围。

因此,正如问题评论中详细指出的那样,在正常的事件过程中,不可能通过参数列表将空字节发送给程序,因为空字节被解释为每个参数的结尾。

通过特别协议

这并没有留下太多的回旋余地。但是,如果调用/调用程序和被调用/调用程序都同意约定,那么即使有标准施加的限制,您也可以将任意二进制数据(包括任意空字节序列)传递给被调用程序 -直到实现对参数列表长度的限制。

公约必须遵循以下原则:

  • 所有参数(除了argv[0]被忽略的 和最后一个参数argv[argc-1])都包含一个非空字节流,后跟一个空值。
  • 如果您需要相邻的空值,则必须在命令行上提供空参数。
  • 如果需要尾随空值,则必须提供空参数作为命令行上的最后一个参数。

这可能会导致一个程序,例如 ( null19.c):

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void hex_dump(const char *tag, size_t size, const char *buffer);

int main(int argc, char **argv)
{
    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s arg1 [arg2 '' arg4 ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    size_t len_args = 0;
    for (int i = 1; i < argc; i++)
        len_args += strlen(argv[i]) + 1;

    char buffer[len_args];

    size_t offset = 0;
    for (int i = 1; i < argc; i++)
    {
        size_t arglen = strlen(argv[i]) + 1;
        memmove(buffer + offset, argv[i], strlen(argv[i]) + 1);
        offset += arglen;
    }
    assert(offset != 0);
    offset--;

    hex_dump("Argument list", offset, buffer);
    return 0;
}

static inline size_t min_size(size_t x, size_t y) { return (x < y) ? x : y; }

static void hex_dump(const char *tag, size_t size, const char *buffer)
{
    printf("%s (%zu):\n", tag, size);
    size_t offset = 0;
    while (size != 0)
    {
        printf("0x%.4zX:", offset);
        size_t count = min_size(16, size);
        for (size_t i = 0; i < count; i++)
            printf(" %.2X", buffer[offset + i] & 0xFF);
        putchar('\n');
        size -= count;
        offset += count;
    }
}

这可以使用以下方法调用:

$ ./null19 '1234' '5678' '' '' '' '' 'def0' ''
Argument list (19):
0x0000: 31 32 33 34 00 35 36 37 38 00 00 00 00 00 64 65
0x0010: 66 30 00
$

第一个参数被认为由 5 个字节组成——四个数字和一个空字节。第二个类似。第三到第六个参数每个代表一个空字节(如果需要大量连续的空字节,这会很痛苦),然后是另一个五个字节的字符串(三个字母,一个数字,一个空字节)。最后一个参数为空,但确保最后有一个空字节。如果省略,输出将不包括最终的终端空字节。

$ ./null19 '1234' '5678' '' '' '' '' 'def0' 
Argument list (18):
0x0000: 31 32 33 34 00 35 36 37 38 00 00 00 00 00 64 65
0x0010: 66 30
$

这与以前相同,只是数据中没有尾随空字节。问题中的两个示例很容易处理:

$ ./null19 $(printf "1\x0123")
Argument list (4):
0x0000: 31 01 32 33
$ ./null19 1 23
Argument list (4):
0x0000: 31 00 32 33
$

假设仅空字符串被识别为有效参数,这在标准范围内严格工作。实际上,这些参数在内存中已经是连续的,因此在许多平台上可能可以避免将复制阶段复制到缓冲区中。但是,该标准并未规定参数字符串在内存中连续布局。

如果您需要二进制数据的多个参数,您可以修改约定。例如,您可以采用一个字符串的控制参数,该参数指示有多少后续物理参数构成一个逻辑二进制参数。

所有这些都依赖于程序按照约定解释参数列表。这不是一个真正的通用解决方案。


推荐阅读