该指令:
if(*ptr)
cmd[strlen(cmd)]=' ';
else
cmd[strlen(cmd)]='\0';
将 break cmd
,因为它将覆盖其零终止。请尝试:
l = strlen(cmd);
if (*ptr) {
cmd[l++] = ' ';
}
cmd[l] = 0x0;
这将附加一个空格,并且零终止字符串。实际上,由于它已经为零终止,您可以做得更好:
if (*ptr) {
int l = strlen(cmd);
cmd[l++] = ' ';
cmd[l ] = 0x0;
}
更新
一个更好的选择可能是这样的:
int main(int argc, char *argv[])
{
char cmd[255]="tcc/tcc.exe";
char **ptr=argv+1;
for (ptr = argv+1; *ptr; ptr++)
{
strncat(cmd, " ", sizeof(cmd)-strlen(cmd));
strncat(cmd, *ptr, sizeof(cmd)-strlen(cmd));
}
printf("String: '%s'.\n", cmd);
return 0;
}
我们strncat()
用来检查我们没有超出cmd
缓冲区,并且空间被提前应用。这样,字符串末尾就没有多余的空间了。
确实strncat()
比直接赋值要慢一点cmd[]
,但考虑到安全和调试时间,我觉得还是值得的。
更新 2
好的,所以让我们尝试快速完成此操作。我们跟踪变量中应该cmd
有什么长度,并复制比它稍快的字符串,既不检查字符串长度,也不复制字符串末尾的额外零。memcpy()
strcpy()
(这节省了一些东西 - 请记住,strcat()
必须隐式计算它strlen
的两个参数。这里我们保存它)。
int main(int argc, char *argv[])
{
#define MAXCMD 255
char cmd[MAXCMD]="tcc/tcc.exe";
int cmdlen = strlen(cmd);
char **ptr=argv+1;
for (ptr = argv+1; *ptr; ptr++)
{
/* How many bytes do we have to copy? */
int l = strlen(*ptr);
/* STILL, this check HAS to be done, or the program is going to crash */
if (cmdlen + 1 + l + 1 < MAXCMD)
{
/* No danger of crashing */
cmd[cmdlen++] = ' ';
memcpy(cmd + cmdlen, *ptr, l);
cmdlen += l;
}
else
{
printf("Buffer too small!\n");
}
}
cmd[cmdlen] = 0x0;
printf("String: '%s'.\n", cmd);
return 0;
}
更新 3 - 不是很推荐,但很有趣
可以尝试比编译器的通常内置strlen
和memcpy
指令更聪明(文件位于:“Bad Ideas”下),并且完全不使用 strlen()。这转化为更小的内部循环,并且当strlen
和memcpy
通过库调用实现时,性能更快(看起来,没有堆栈帧!)。
int main(int argc, char *argv[])
{
#define MAXCMD 254
char cmd[MAXCMD+1]="tcc/tcc.exe";
int cmdlen = 11; // We know initial length of "tcc/tcc.exe"!
char **ptr;
for (ptr = argv+1; *ptr; ptr++)
{
cmd[cmdlen++] = ' ';
while(**ptr) {
cmd[cmdlen++] = *(*ptr)++;
if (MAXCMD == cmdlen)
{
fprintf(stderr, "BUFFER OVERFLOW!\n");
return -1;
}
}
}
cmd[cmdlen] = 0x0;
printf("String: '%s'.\n", cmd);
return 0;
}
讨论 - 不那么有趣
我从我认为目光短浅的教授那里收到的许多讲座无耻地抄袭,直到他们每次都被证明是正确的。
这里的问题是要准确界定我们在做什么——这棵特定的树属于哪个森林。
我们正在构建一个将被提供给exec()
调用的命令行,这意味着操作系统将不得不构建另一个进程环境并分配和跟踪资源。让我们退后一步:将运行一个大约需要 1 毫秒的操作,我们正在为其提供一个可能需要 10 微秒而不是 20 微秒的循环。
我们对内部循环的 20:10(即 50%!)改进转化为 1020:1010(即大约 1%)只是整个流程启动操作。让我们假设这个过程需要半秒 - 五百毫秒 - 才能完成,我们正在查看 500020:500010 或 0.002% 的改进,这与从未充分记住的http://en.wikipedia.org/wiki一致/Amdahl%27s_law。
或者让我们换一种说法。一年后,我们将运行这个程序,比如说,十亿次。现在节省的这 10 微秒转化为惊人的 10.000 秒,或大约两个小时零三分之二。我们开始大谈特谈,除了为了获得这个结果,我们花费了 16 个小时的编码、检查和调试 :-)
双重strncat()
解决方案(实际上是最慢的)提供更易于阅读、理解和修改的代码。并重复使用。上面最快的解决方案隐含地依赖于分隔符是一个字符,而这一事实并不是立即显而易见的。这意味着重用以“,”作为分隔符的最快解决方案(假设我们需要它用于 CSV 或 SQL)现在将引入一个微妙的错误。
在设计一个算法或一段代码时,明智的做法是不仅要考虑代码的紧密度和本地(“钥匙孔”)性能,还要考虑以下因素:
- 单曲如何影响整体的表现。将 10% 的开发时间花在不到10% 的总体目标上是没有意义的。
- 编译器解释它是多么容易(并且无需我们进一步努力对其进行优化,甚至可能专门针对不同的平台进行优化——所有这些都是免费的!)
- 几天、几周或几个月后,我们将多么容易理解它。
- 代码的非特定性和健壮性如何,允许在其他地方重用它(DRY)。
- 它的意图有多清晰 - 允许稍后对其进行重新设计,或者用相同意图的不同实现 ( DRAW ) 替换。
一个微妙的错误
这是为了回答威廉莫里斯的问题,所以我将使用他的代码,但我的代码也有同样的问题(实际上,我的问题 - 并非完全是无意的 -更糟)。
这是 William 代码的原始功能:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
#define CMD "tcc/tcc.exe"
char cmd[255] = CMD;
char *s = cmd + sizeof CMD - 1;
const char *end = cmd + sizeof cmd - 1;
// Cycle syntax modified to pre-C99 - no consequences on our code
int i;
for (i = 1; i < argc; ++i) {
size_t len = strlen(argv[i]);
if (s + len >= end) {
fprintf(stderr, "Buffer overrun!\n");
exit(1);
}
// Here (will) be dragons
//*s++ = '.';
//*s++ = '.';
//*s++ = '.';
*s++ = ' ';
memcpy(s, argv[i], len);
s += len;
}
*s = '\0';
// Get also string length, which should be at most 254
printf("%s: string length is %d\n", cmd, (int)strlen(cmd));
return 0;
}
缓冲区溢出检查验证到目前为止写入的字符串以及尚未写入的字符串一起不超过缓冲区。分隔符本身的长度不计算在内,但事情会以某种方式解决:
size_t len = strlen(argv[i]);
if (s + len >= end) {
fprintf(stderr, "Buffer overrun!\n");
exit(1);
}
现在我们以最快速的方式添加分隔符 - 通过重复戳:
*s++ = ', ';
*s++ = ' ';
现在如果s + len
等于end - 1
,则检查将通过。我们现在添加两个字节。总长度为 s + len + 2,等于 end加一:
tcc/tcc.exe, 它是, 最好的, 时代, 它, 是, 最坏的, 时代, 它, 是, 时代, 的, 智慧, 它, 是, 时代, of, 愚蠢, it, was, the, epoch, of, confidence, it, was, the, epoch, of, incredulity, it, was, the, season, of, Light, it, was: string length is 254
tcc/tcc.exe, 它是, 最好的, 时代, 它, 是, 最坏的, 时代, 它, 是, 时代, 的, 智慧, 它, 是, 时代, of, 愚蠢, it, was, the, epoch, of, 信念, it, was, the, the, epoch, of, incredulity, it, was, the, season, of, Light, it, ouch : string length is 255
使用更长的分隔符,例如“...”,问题就更加明显了:
tcc/tcc.exe... 它... 是... 最好的... 次... 它... 是... 最... 最差的... ..时代……它……是……智慧的……时代……它……是……时代……的……愚蠢……它……是……信仰的……时代……它……曾经……更长:字符串长度为
257
在我的版本中,检查需要完全匹配的事实会导致灾难性的结果,因为一旦缓冲区溢出,匹配将始终失败并导致大量内存覆盖。
如果我们修改我的版本
if (cmdlen >= MAXCMD)
我们将得到一个总是拦截缓冲区溢出的代码,但仍然不能阻止它们直到分隔符的长度减去 2;即,假设的 20 字节长的分隔符在被捕获之前可能会覆盖 18 字节cmd
的缓冲区。
我要指出,这并不是说我的代码有一个灾难性的错误(因此,一旦修复,它将永远快乐);关键是代码的结构方式是,为了加快速度,一个危险的错误很容易被忽视,或者在重用看起来“安全且经过测试”的东西时很容易引入相同的错误代码。这是一种最好避免的情况。
(我现在要坦白,并承认我自己很少这样做......而且经常仍然不这样做)。