C程序向MariaDB插入含0x80以上字符的字符串失败问题排查
你的核心问题集中在两点:SQL拼接时的编码不匹配,以及双字节宽字符到多字节的转换失败,导致含特殊字符的字符串无法被数据库识别,同时直接拼接SQL还存在严重的注入风险。
从GDB输出看,\260是八进制的0xB0,对应Latin-1(ISO-8859-1)的度数符号,但你的数据库使用utf8mb3编码(UTF-8的子集),而UTF-8中度数符号的正确编码是0xC2 0xB0。直接传入0xB0会被数据库判定为无效UTF-8序列,最终导致静默插入失败(MariaDB默认会忽略或截断无效字符)。
另外,原文本是低字节在前的双字节固定宽字符(即UTF-16LE编码),wctombs()转换失败大概率是因为程序当前locale不是UTF-8,或是未正确处理宽字符的字节序。
1. 替换SQL拼接,使用参数化查询(必须执行)
直接用sprintf拼接SQL不仅会引发编码问题,还存在SQL注入风险。MariaDB的C API提供了参数化查询接口,能自动处理编码和特殊字符,是最优解决方案。
示例代码:
MYSQL_STMT *stmt; MYSQL_BIND bind[3]; char drawing_buf[64]; long long eid_val; char txt_utf8_buf[1024]; unsigned long drawing_len, txt_len; // 初始化预编译语句 stmt = mysql_stmt_init(sqlconnect); if (!stmt) { // 处理初始化错误 } const char *query = "INSERT INTO text(drawing, eID, txt) VALUES(?, ?, ?)"; if (mysql_stmt_prepare(stmt, query, strlen(query))) { // 处理预编译错误,可调用mysql_error(sqlconnect)查看详情 } // 绑定参数,先清空绑定结构 memset(bind, 0, sizeof(bind)); // 绑定drawing字段(字符串类型) strcpy(drawing_buf, drawing_number); drawing_len = strlen(drawing_buf); bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = drawing_buf; bind[0].buffer_length = sizeof(drawing_buf); bind[0].length = &drawing_len; // 绑定eID字段(长整型) eid_val = element_handle; bind[1].buffer_type = MYSQL_TYPE_LONG; bind[1].buffer = &eid_val; // 绑定txt字段(已转换为UTF-8的字符串) txt_len = strlen(txt_utf8_buf); bind[2].buffer_type = MYSQL_TYPE_STRING; bind[2].buffer = txt_utf8_buf; bind[2].buffer_length = sizeof(txt_utf8_buf); bind[2].length = &txt_len; // 执行参数化语句 if (mysql_stmt_bind_param(stmt, bind)) { // 处理绑定错误 } if (mysql_stmt_execute(stmt)) { // 处理执行错误,调用mysql_error(sqlconnect)获取具体信息 } // 清理资源 mysql_stmt_close(stmt);
2. 正确转换UTF-16LE到UTF-8
原文本是UTF-16LE编码,不能直接依赖wctombs()(因wchar_t的字节序和大小存在平台差异),推荐两种转换方式:
方法1:使用libiconv库(跨平台通用)
libiconv是专门的编码转换库,可直接处理UTF-16LE到UTF-8的转换:
#include <iconv.h> int utf16le_to_utf8(const char *utf16le_buf, size_t utf16le_len, char *utf8_buf, size_t utf8_buf_size) { iconv_t cd = iconv_open("UTF-8", "UTF-16LE"); if (cd == (iconv_t)-1) { return -1; } char *in_ptr = (char *)utf16le_buf; size_t in_left = utf16le_len; char *out_ptr = utf8_buf; size_t out_left = utf8_buf_size; size_t ret = iconv(cd, &in_ptr, &in_left, &out_ptr, &out_left); iconv_close(cd); if (ret == (size_t)-1) { return -1; } // 添加字符串结束符 *out_ptr = '\0'; return utf8_buf_size - out_left; }
调用该函数将原双字节文本转换为UTF-8后,再传入参数化查询的绑定缓冲区。
方法2:手动转换(无依赖,适合简单场景)
若不想依赖第三方库,可手动处理BMP范围内的字符转换(比如度数符号的UTF-16LE编码是0xB0 0x00):
#include <stdint.h> size_t utf16le_to_utf8(const uint16_t *utf16le, size_t count, char *utf8, size_t utf8_size) { size_t utf8_idx = 0; for (size_t i = 0; i < count; i++) { uint16_t wc = utf16le[i]; // 小端字节序,直接取uint16_t即为正确码点 if (wc < 0x80) { if (utf8_idx + 1 > utf8_size) return -1; utf8[utf8_idx++] = (char)wc; } else if (wc < 0x800) { if (utf8_idx + 2 > utf8_size) return -1; utf8[utf8_idx++] = 0xC0 | ((wc >> 6) & 0x1F); utf8[utf8_idx++] = 0x80 | (wc & 0x3F); } else { // 处理BMP以外的字符(可选,根据你的业务场景决定是否需要) if (utf8_idx + 3 > utf8_size) return -1; utf8[utf8_idx++] = 0xE0 | ((wc >> 12) & 0x0F); utf8[utf8_idx++] = 0x80 | ((wc >> 6) & 0x3F); utf8[utf8_idx++] = 0x80 | (wc & 0x3F); } } utf8[utf8_idx] = '\0'; return utf8_idx; }
注意:原双字节数组需按uint16_t类型读取,小端字节序下直接强制转换即可。
3. 确保数据库连接字符集为utf8mb3
在建立数据库连接后,设置连接的字符集与数据库一致,避免编码转换冲突:
mysql_set_character_set(sqlconnect, "utf8mb3");
- 禁止用字符串拼接构造SQL,参数化查询是解决编码和注入问题的核心方案。
- 编码转换必须明确源编码(UTF-16LE)和目标编码(UTF-8),不要依赖默认locale,否则会出现平台差异。
- 插入失败时,调用
mysql_error(sqlconnect)获取具体错误信息,不要只看表面的“无报错”(MariaDB可能静默处理无效字符)。
内容的提问来源于stack exchange,提问作者Dave Coventry

