我在C中有一个字符串创建函数,该函数接受array of structs
作为其参数,并根据预定义的格式(例如python中的列表列表)输出字符串。
这是功能
typedef struct
{
PacketInfo_t PacketInfo;
char Gnss60[1900];
//and other stuff...
} Track_json_t;
typedef struct
{
double latitude;
double longitude;
} GPSPoint_t;
typedef struct
{
UInt16 GPS_StatusCode;
UInt32 fixtime;
GPSPoint_t point;
double altitude;
unsigned char GPS_Satilite_Num;
} GPS_periodic_t;
unsigned short SendTrack()
{
Track_json_t i_sTrack_S;
memset(&i_sTrack_S, 0x00, sizeof(Track_json_t));
getEvent_Track(&i_sTrack_S);
//Many other stuff added to the i_sTrack_S struct...
//Make a JSON format out of it
BuildTrackPacket_json(&i_sTrack_S, XPORT_MODE_GPRS);
}
Track_json_t *getEvent_Track(Track_json_t *trk)
{
GPS_periodic_t l_gps_60Sec[60];
memset(&l_gps_60Sec, 0x00,
sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);
getLastMinGPSdata(l_gps_60Sec, o_gps_base);
get_gps60secString(l_gps_60Sec, trk->Gnss60);
return trk;
}
void get_gps60secString(GPS_periodic_t input[60], char *output)
{
int i = 0;
memcpy(output, "[", 1); ///< Copy the first char as [
char temp[31];
for (i = 0; i < 59; i++) { //Run for n-1 elements
memset(temp, 0, sizeof(temp));
snprintf(temp, sizeof(temp), "[%0.8f,%0.8f],",
input[i].point.latitude, input[i].point.longitude);
strncat(output, temp, sizeof(temp));
}
memset(temp, 0, sizeof(temp)); //assign last element
snprintf(temp, sizeof(temp), "[%0.8f,%0.8f]]",
input[i].point.latitude, input[i].point.longitude);
strncat(output, temp, sizeof(temp));
}
因此,函数的输出必须为格式字符串
[[12.12345678,12.12345678],[12.12345678,12.12345678],...]
但是有时候我会得到一个看起来像
的字符串[[[12.12345678,12.12345678], [55.01 ] [12.12345678,12.12345678],...]
[[21.28211567,84.13454083], [21.28211533,21.22 [21.28211517,84.13454000],..]
以前,我在函数get_gps60secString
上发生了缓冲区溢出,我通过使用snprintf
和strncat
来解决了这个问题。
注意:这是一个嵌入式应用程序,该错误每天发生一次或两次(共1440个数据包)
问题
1.这可能是由snprintf / strncat进程中的中断引起的吗?
2.这可能是由内存泄漏,覆盖堆栈或其他原因导致的分段问题引起的吗?
基本上,我想了解是什么原因导致了字符串损坏。
很难找到原因并修复此错误。
编辑:
我使用了chux's
函数。以下是最小,完整和可验证的示例
/*
* Test code for SO question https://stackoverflow.com/questions/5216413
* A Minimal, Complete, and Verifiable Example
*/
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>
typedef unsigned short UInt16;
typedef unsigned long UInt32;
#define GPS_PERIODIC_ARRAY_SIZE 60
#define GPS_STRING_SIZE 1900
/* ---------------------- Data Structs --------------------------*/
typedef struct
{
char Gnss60[GPS_STRING_SIZE];
} Track_json_t;
typedef struct
{
double latitude;
double longitude;
} GPSPoint_t;
typedef struct
{
UInt16 GPS_StatusCode;
UInt32 fixtime;
GPSPoint_t point;
double altitude;
unsigned char GPS_Satilite_Num;
} GPS_periodic_t;
/* ----------------------- Global --------------------------------*/
FILE *fptr; //Global file pointer
int res = 0;
int g_last = 0;
GPS_periodic_t l_gps_60Sec[GPS_PERIODIC_ARRAY_SIZE];
/* ----------------------- Function defs --------------------------*/
/* At signal interrupt this function is called.
* Flush and close the file. And safly exit the program */
void userSignalInterrupt()
{
fflush(fptr);
fclose(fptr);
res = 1;
exit(0);
}
/* @brief From the array of GPS structs we create a string of the format
* [[lat,long],[lat,long],..]
* @param input The input array of GPS structs
* @param output The output string which will contain lat, long
* @param sz Size left in the output buffer
* @return 0 Successfully completed operation
* 1 Failed / Error
*/
int get_gps60secString(GPS_periodic_t input[GPS_PERIODIC_ARRAY_SIZE],
char *output, size_t sz)
{
int cnt = snprintf(output, sz, "[");
if (cnt < 0 || cnt >= sz)
return 1;
output += cnt;
sz -= cnt;
int i = 0;
for (i = 0; i < GPS_PERIODIC_ARRAY_SIZE; i++) {
cnt = snprintf(output, sz, "[%0.8f,%0.8f]%s",
input[i].point.latitude, input[i].point.longitude,
i + 1 == GPS_PERIODIC_ARRAY_SIZE ? "" : ",");
if (cnt < 0 || cnt >= sz)
return 1;
output += cnt;
sz -= cnt;
}
cnt = snprintf(output, sz, "]");
if (cnt < 0 || cnt >= sz)
return 1;
return 0; // no error
}
/* @brief Create a GPS struct with data for testing. It will populate the
* point field of GPS_periodic_t. Lat starts from 0.0 and increases by 1*10^(-8)
* and Long will dstart at 99.99999999 and dec by 1*10^(-8)
*
* @param o_gps_60sec Output array of GPS structs
*/
void getLastMinGPSdata(GPS_periodic_t *o_gps_60sec)
{
//Fill in GPS related data here
int i = 0;
double latitude = o_gps_60sec[0].point.latitude;
double longitude = o_gps_60sec[0].point.longitude;
for (i = 0; i < 60; i++)
{
o_gps_60sec[i].point.latitude = latitude + (0.00000001 * (float)g_last +
0.00000001 * (float)i);
o_gps_60sec[i].point.longitude = longitude - (0.00000001 * (float)g_last +
0.00000001 * (float)i);
}
g_last = 60;
}
/* @brief Get the GPS data and convert it into a string
* @param trk Track structure with GPS string
*/
int getEvent_Track(Track_json_t *trk)
{
getLastMinGPSdata(l_gps_60Sec);
get_gps60secString(l_gps_60Sec, trk->Gnss60, GPS_STRING_SIZE);
return 0;
}
int main()
{
fptr = fopen("gpsAno.txt", "a");
if (fptr == NULL) {
printf("Error!!\n");
exit(1);
}
//Quit at signal interrupt
signal(SIGINT, userSignalInterrupt);
Track_json_t trk;
memset(&l_gps_60Sec, 0x00, sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);
//Init Points to be zero and 99.99999999
int i = 0;
for (i = 0; i < 60; i++) {
l_gps_60Sec[i].point.latitude = 00.00000000;
l_gps_60Sec[i].point.longitude = 99.99999999;
}
do {
memset(&trk, 0, sizeof(Track_json_t));
getEvent_Track(&trk);
//Write to file
fprintf(fptr, "%s", trk.Gnss60);
fflush(fptr);
sleep(1);
} while (res == 0);
//close and exit
fclose(fptr);
return 0;
}
注意:以上代码未重新创建错误。
因为这没有strcat
陷阱。
我在嵌入式应用程序中测试了此功能。
通过此操作,我发现snprintf
返回错误,并且创建的字符串最终为:
[17.42401750,78.46098717],[17.42402083, 53.62
它到此结束(由于return 1
)。
这是否意味着传递到snprints
的数据已损坏?这是一个浮点值。它怎么会被损坏?
解决方案
自从我将sprintf
函数更改为不直接处理64位数据的函数以来,从未发现该错误。
这里是功能modp_dtoa2
/** \brief convert a floating point number to char buffer with a
* variable-precision format, and no trailing zeros
*
* This is similar to "%.[0-9]f" in the printf style, except it will
* NOT include trailing zeros after the decimal point. This type
* of format oddly does not exists with printf.
*
* If the input value is greater than 1<<31, then the output format
* will be switched exponential format.
*
* \param[in] value
* \param[out] buf The allocated output buffer. Should be 32 chars or more.
* \param[in] precision Number of digits to the right of the decimal point.
* Can only be 0-9.
*/
void modp_dtoa2(double value, char* str, int prec)
{
/* if input is larger than thres_max, revert to exponential */
const double thres_max = (double)(0x7FFFFFFF);
int count;
double diff = 0.0;
char* wstr = str;
int neg= 0;
int whole;
double tmp;
uint32_t frac;
/* Hacky test for NaN
* under -fast-math this won't work, but then you also won't
* have correct nan values anyways. The alternative is
* to link with libmath (bad) or hack IEEE double bits (bad)
*/
if (! (value == value)) {
str[0] = 'n'; str[1] = 'a'; str[2] = 'n'; str[3] = '\0';
return;
}
if (prec < 0) {
prec = 0;
} else if (prec > 9) {
/* precision of >= 10 can lead to overflow errors */
prec = 9;
}
/* we'll work in positive values and deal with the
negative sign issue later */
if (value < 0) {
neg = 1;
value = -value;
}
whole = (int) value;
tmp = (value - whole) * pow10[prec];
frac = (uint32_t)(tmp);
diff = tmp - frac;
if (diff > 0.5) {
++frac;
/* handle rollover, e.g. case 0.99 with prec 1 is 1.0 */
if (frac >= pow10[prec]) {
frac = 0;
++whole;
}
} else if (diff == 0.5 && ((frac == 0) || (frac & 1))) {
/* if halfway, round up if odd, OR
if last digit is 0. That last part is strange */
++frac;
}
/* for very large numbers switch back to native sprintf for exponentials.
anyone want to write code to replace this? */
/*
normal printf behavior is to print EVERY whole number digit
which can be 100s of characters overflowing your buffers == bad
*/
if (value > thres_max) {
sprintf(str, "%e", neg ? -value : value);
return;
}
if (prec == 0) {
diff = value - whole;
if (diff > 0.5) {
/* greater than 0.5, round up, e.g. 1.6 -> 2 */
++whole;
} else if (diff == 0.5 && (whole & 1)) {
/* exactly 0.5 and ODD, then round up */
/* 1.5 -> 2, but 2.5 -> 2 */
++whole;
}
//vvvvvvvvvvvvvvvvvvv Diff from modp_dto2
} else if (frac) {
count = prec;
// now do fractional part, as an unsigned number
// we know it is not 0 but we can have leading zeros, these
// should be removed
while (!(frac % 10)) {
--count;
frac /= 10;
}
//^^^^^^^^^^^^^^^^^^^ Diff from modp_dto2
// now do fractional part, as an unsigned number
do {
--count;
*wstr++ = (char)(48 + (frac % 10));
} while (frac /= 10);
// add extra 0s
while (count-- > 0) *wstr++ = '0';
// add decimal
*wstr++ = '.';
}
// do whole part
// Take care of sign
// Conversion. Number is reversed.
do *wstr++ = (char)(48 + (whole % 10)); while (whole /= 10);
if (neg) {
*wstr++ = '-';
}
*wstr='\0';
strreverse(str, wstr-1);
}
答案 0 :(得分:4)
这是我关于C语言中安全字符串处理的毫无疑问的指南的一部分(通常是)。通常,我将促进动态内存分配,而不是固定长度的字符串,但是在这种情况下,我假设在嵌入式环境中有问题的。 (尽管应该始终检查类似的假设。)
所以,首先要做的是
必须明确告知任何在缓冲区中创建字符串的函数。这是不能商量的。
很明显,除非缓冲区知道缓冲区的结束位置,否则填充缓冲区的函数无法检查缓冲区是否溢出。 “希望缓冲区足够长”不是可行的策略。如果每个人都仔细阅读了文档(不是这样)并且所需的长度从不改变(会的话),那么“记录所需的缓冲区长度”就可以了。剩下的只有一个额外的参数,该参数应为size_t
类型(因为这是C库函数中需要长度的缓冲区长度的类型)。
忘记存在strncpy
和strncat
。还要忘掉strcat
。他们不是您的朋友。
strncpy
设计用于特定的用例:确保初始化整个固定长度的缓冲区。它不是为普通字符串设计的,并且由于不能保证输出为NUL终止,因此不会产生字符串。
如果您要进行NUL终止,则最好使用memmove
或memcpy
,如果您知道源和目标不重叠,则几乎总是案子。由于您希望memmove
在短字符串的字符串末尾停止(strncpy
不会 这样做),因此请首先使用{{1 }}:strnlen
的长度最大,这正是您要移动最大字符数时想要的长度。
示例代码:
strnlen
/* Safely copy src to dst where dst has capacity dstlen. */
if (dstlen) {
/* Adjust to_move will have maximum value dstlen - 1 */
size_t to_move = strnlen(src, dstlen - 1);
/* copy the characters */
memmove(dst, src, to_move);
/* NUL-terminate the string */
dst[to_move] = 0;
}
的语义稍显合理,但实际上几乎没有用,因为要使用它,您已经必须知道可以复制多少个字节。为了知道这一点,实际上,您需要知道输出缓冲区中还有多少空间,并且需要知道副本将在输出缓冲区中的何处开始。 [注1]。但是,如果您已经知道复制将从何处开始,那么从头开始搜索缓冲区以找到复制点有什么意义?而且,如果您让strncat
进行搜索,那么您如何确定先前计算的起点正确?
在上面的代码片段中,我们已经计算出副本的长度。我们可以扩展它以执行附加操作而无需重新扫描:
strncat
创建字符串之后,可能是我们想要/* Safely copy src1 and then src2 to dst where dst has capacity dstlen. */
/* Assumes that src1 and src2 are not contained in dst. */
if (dstlen) {
/* Adjust to_move will have maximum value dstlen - 1 */
size_t to_move = strnlen(src1, dstlen - 1);
/* Copy the characters from src1 */
memcpy(dst, src1, to_move);
/* Adjust the output pointer and length */
dst += to_move;
dstlen -= to_move;
/* Now safely copy src2 to just after src1. */
to_move = strnlen(src2, dstlen - 1);
memcpy(dst, src2, to_move);
/* NUL-terminate the string */
dst[to_move] = 0;
}
和dst
的原始值,也可能是我们想知道我们插入dstlen
的字节数在所有。在这种情况下,我们可能希望在进行复制之前先复制这些变量,然后保存累积的移动总数。
以上假设我们从一个空的输出缓冲区开始,但事实并非如此。由于我们仍然需要知道副本将从何处开始,以便知道可以在末尾添加多少个字符,因此我们仍然可以使用dst
;我们只需要先扫描输出缓冲区以找到复制点即可。 (只有在没有其他选择的情况下,才执行此操作。循环进行此操作而不是记录下一个复制点是Shlemiel the Painter's algorithm。)
memcpy
拥抱/* Safely append src to dst where dst has capacity dstlen and starts
* with a string of unknown length.
*/
if (dstlen) {
/* The following code will "work" even if the existing string
* is not correctly NUL-terminated; the code will not copy anything
* from src, but it will put a NUL terminator at the end of the
* output buffer.
*/
/* Figure out where the existing string ends. */
size_t prefixlen = strnlen(dst, dstlen - 1);
/* Update dst and dstlen */
dst += prefixlen;
dstlen -= prefixlen;
/* Proceed with the append, as above. */
size_t to_move = strnlen(src, dstlen - 1);
memmove(dst, src, to_move);
dst[to_move] = 0;
}
。真的是你的朋友但是请务必检查其返回值。
如上所述,使用snprintf
有点尴尬。它要求您手动检查缓冲区的长度是否不为零(否则,减去1将会是灾难性的,因为该长度是无符号的),并且它要求您手动NUL终止输出缓冲区,这很容易忘记,并且是许多缓冲区的来源错误。这是非常有效的,但是有时需要牺牲一点效率,以便您的代码更易于编写,阅读和验证。
然后直接将我们引向memmove
。例如,您可以替换:
snprintf
简单得多
if (dstlen) {
size_t to_move = strnlen(src, dstlen - 1);
memcpy(dst, src, to_move);
dst[to_move] = 0;
}
这一切都做得到:检查int copylen = snprintf(dst, dstlen, "%s", src);
是否不为0;仅复制dstlen
中适合src
的字符,并正确NUL终止dst
(除非dst
为0)。而且成本极低;解析格式字符串dstlen
所需的时间很少,大多数实现已针对这种情况进行了优化。 [注2]
但是"%s"
不是万能药。仍然有一些非常重要的警告。
首先,snprintf
的文档明确指出,不允许任何输入参数重叠输出范围。 (因此它代替了snprintf
而不是memcpy
。)请记住,重叠包括NUL终止符,因此以下代码尝试将memmove
中的字符串加倍,从而导致 Undefined Behavior :
str
第二次调用char str[BUFLEN];
/* Put something into str */
get_some_data(str, BUFLEN);
/* DO NOT DO THIS: input overlaps output */
int result = snprintf(str, BUFLEN, "%s%s", str, str);
/* DO NOT DO THIS EITHER; IT IS STILL UB */
size_t len = strnlen(str, cap - 1);
int result = snprintf(str + len, cap - len, "%s", str);
的问题在于,终止snprintf
的NUL恰好位于输出缓冲区的第一个字节str
处。这是重叠的,因此是非法的。
关于str + len
的第二个重要说明是它返回一个值,该值不能忽略。返回的值不是snprintf
创建的字符串的长度。这是如果不将字符串截断以适合输出缓冲区的长度。
如果没有截断发生,那么结果就是结果的长度,必须严格小于输出缓冲区的大小(因为必须有空间容纳NUL终止符,即不认为是结果长度的一部分。)您可以使用此事实来检查是否发生了截断:
snprintf
例如,可以使用它来使用较大的动态分配的缓冲区(大小为if (result >= dstlen) /* Output was truncated */
;不要忘记需要NUL终止)来重做snprintf
。
但是请记住,结果是result + 1
,即带符号的值。这意味着int
无法应付很长的字符串。在嵌入式代码中这不太可能成为问题,但是在可以想到字符串超过2GB的系统上,您可能无法安全地使用snprintf
中的%s
格式。这也意味着允许snprintf
返回负值以指示错误。 snprintf
的很旧的实现返回-1表示截断,或者响应于缓冲区长度为0的调用。根据C99(不是Posix的最新版本),这不是标准行为,但是您应该为此做好准备。
如果缓冲区长度参数太大而不能容纳(带符号的)snprintf
,则snprintf
的符合标准的实现将返回负值;对于我来说,如果缓冲区长度确定可以,但是对于int
来说,未截断的长度太大,预期返回的值是多少呢?如果您使用的转换导致编码错误,则也会返回负值;例如,int
转换,其对应的参数包含一个不能转换为多字节(通常为UTF-8)序列的整数。
简而言之,您应该始终检查%lc
的返回值(如果您不这样做,则最新的gcc / glibc版本将产生警告),并且应该准备使其为负数。
因此,我们已经编写了所有产生坐标对的字符串的函数:
snprintf
您经常会看到这样的代码:
/* Arguments:
* buf the output buffer.
* buflen the capacity of buf (including room for trailing NUL).
* points a vector of struct Point pairs.
* npoints the number of objects in points.
* Description:
* buf is overwritten with a comma-separated list of points enclosed in
* square brackets. Each point is output as a comma-separated pair of
* decimal floating point numbers enclosed in square brackets. No more
* than buflen - 1 characters are written. Unless buflen is 0, a NUL is
* written following the (possibly-truncated) output.
* Return value:
* If the output buffer contains the full output, the number of characters
* written to the output buffer, not including the NUL terminator.
* If the output was truncated, (size_t)(-1) is returned.
*/
size_t sprint_points(char* buf, size_t buflen,
struct Point const* points, size_t npoints)
{
if (buflen == 0) return (size_t)(-1);
size_t avail = buflen;
char delim = '['
while (npoints) {
int res = snprintf(buf, avail, "%c[%f,%f]",
delim, points->lat, points->lon);
if (res < 0 || res >= avail) return (size_t)(-1);
buf += res; avail -= res;
++points; --npoints;
delim = ',';
}
if (avail <= 1) return (size_t)(-1);
strcpy(buf, "]");
return buflen - (avail - 1);
}
告诉strncat(dst, src, sizeof(src)); /* NEVER EVER DO THIS! */
不要在strncat
中追加超出src
不能容纳的字符显然是没有意义的(除非src
没有正确地以NUL终止,在这种情况下,您有更大的问题)。更重要的是,它不会做任何绝对的事情来保护您免受输出缓冲区末尾的影响,因为您没有做任何事情来检查src
是否有足够的空间容纳所有这些字符。因此,它所做的一切就是摆脱关于dst
不安全的编译器警告。由于此代码与strcat
一样完全不安全,因此警告可能会更好。
您甚至可能会找到一个编译器,它了解strcat
足以在编译时解析格式字符串,因此带来的便利是完全免费的。 (而且,如果您当前的编译器不这样做,那么无疑将是将来的版本。)与使用snprintf
系列一样,您应该从不尝试通过以下方式来节省键击:
省略格式字符串(用*printf
代替snprintf(dst, dstlen, src)
。)不安全(如果snprintf(dst, dstlen, "%s", src)
包含不重复的src
,则行为不确定)。而且慢得多,因为库函数必须解析要复制的整个字符串以寻找百分号,而不仅仅是将其复制到输出中。
答案 1 :(得分:1)
代码使用的函数期望指向 string 的指针,但并不总是将指向 strings 的指针作为参数传递。
在snprintf输出中看到的流浪字符
字符串必须具有终止的空字符。
strncat(char *, ....
期望第一个参数是指向字符串的指针。 memcpy(output, "[",1);
不能保证这一点。 @Jeremy
memcpy(output, "[",1);
...
strncat(output, temp,sizeof(temp));
这是流浪字符的候选来源。
strncat(...., ..., size_t size).
本身是一个问题,因为size
是可用于连接的空间量(减去空字符)。 char * output
可用的大小未传递。@Jonathan Leffler。这里strcat()
也可以。
相反,请传递output
可用的大小以防止缓冲区溢出。
#define N 60
int get_gps60secString(GPS_periodic_t input[N], char *output, size_t sz) {
int cnt = snprintf(output, sz, "[");
if (cnt < 0 || cnt >= sz)
return 1;
output += cnt;
sz -= cnt;
int i = 0;
for (i = 0; i < N; i++) {
cnt = snprintf(output, size, "[%0.8f,%0.8f]%s", input[i].point.latitude,
input[i].point.longitude, i + 1 == N ? "" : ",");
if (cnt < 0 || cnt >= sz)
return 1;
output += cnt;
sz -= cnt;
}
cnt = snprintf(output, sz, "]");
if (cnt < 0 || cnt >= sz)
return 1;
return 0; // no error
}
OP发布了更多代码-将进行审查。
显然,缓冲区char *output
在get_gps60secString()
之前预先填充了0,因此memcpy(output, "[",1);
中丢失的 null字符应该不会引起问题-hmmmmmm < / p>
unsigned short SendTrack()
不返回值。 1)使用其结果值为UB。 2)启用所有编译器警告。