☰
AES-256 C语言实现:嵌入式可审计加解密模块
2026/10/15 11:32:08 网站建设 项目流程

简介:本资源是一份轻量级、可直接集成的AES-256对称加密算法C语言实现,面向嵌入式开发工程师、密码学初学者及需要自主可控加解密能力的C/C++开发者,解决在无第三方加密库(如OpenSSL)环境下快速实现高安全性数据保护的问题。压缩包共3个文件(2个C源码 + 1个头文件),总大小仅5KB,结构精简:aes256.c完整实现FIPS-197标准定义的字节代换、行移位、列混淆与密钥扩展等核心轮函数;demo.c提供可编译运行的端到端示例,涵盖密钥初始化、CBC模式加解密调用及明密文验证逻辑;aes256.h统一声明接口与常量,便于工程化引用。已有410人学习下载,代码注释清晰、无外部依赖,适合作为教学范例、IoT设备固件加密模块或安全协议底层组件直接复用,同时附带关键实践提示——涵盖CBC模式选型依据、PKCS#7填充规范及密钥安全管理要点。

1. AES-256 C语言实现:为什么一个“标准算法”在嵌入式和跨平台项目里总要自己手撸加解密模块?

你手头有个资源受限的 MCU,需要安全存储配置参数;或者你在写一个跨平台的本地加密工具,不依赖 OpenSSL 这类重量级库;又或者你在做国密合规预研,得先吃透对称密码底层行为——这时候,AES-256 的 C 语言原生实现就不是“可选项”,而是“绕不开的必答题”。它不像 Python 里cryptography.hazmat.primitives.ciphers一行 import 就能跑,C 层必须直面 S-box 查表、轮密钥扩展、字节代换、行移位、列混合、密钥调度这些硬核细节。很多开发者踩坑后才明白:网上随手搜到的“AES C demo”常缺完整性校验、密钥长度校验、内存越界防护,甚至把 ECB 模式当默认方案用——而 ECB 在真实场景中基本等于明文裸奔。本文不讲 FIPS 标准条文,也不堆砌数学推导,只聚焦一线工程师真正要干的事:用纯 ANSI C(C89 兼容)写一个可验证、可审计、可嵌入、带完整测试向量比对能力的 AES-256 实现,并把最易翻车的 5 个点掰开揉碎讲清楚。适合正在调试 STM32 加密模块、开发 IoT 设备固件、或为国产化环境准备密码基础组件的开发者。


2. 从零构建 AES-256 C 实现:核心结构设计与轮函数拆解

AES-256 是分组长度 128 位(16 字节)、密钥长度 256 位(32 字节)的迭代型分组密码,共 14 轮加密。其安全性不依赖于“算法保密”,而依赖于密钥保密与实现正确性。我们采用查表法(T-tables)实现,兼顾速度与代码简洁性——这是嵌入式领域最主流、最经实战检验的选择。注意:不使用 OpenSSL 或 mbed TLS 等第三方库,所有代码控制在单个.c+.h文件内,无外部依赖。

2.1 整体模块划分与数据结构定义

我们定义两个核心结构体:aes256_context存储轮密钥(uint32_t ek[60],共 14 轮 × 4 个字 + 初始轮密钥 4 个字 = 60),aes256_block封装 16 字节明/密文块(避免指针误操作)。关键约束:所有数组索引严格按 AES 规范下标,不省略任何中间变量,便于调试与静态分析。

// aes256.h #ifndef AES256_H #define AES256_H #include <stdint.h> #include <stddef.h> typedef struct { uint32_t ek[60]; // encryption key schedule (60 words for 14 rounds) } aes256_context; typedef struct { uint8_t b[16]; // 128-bit block } aes256_block; void aes256_init(aes256_context *ctx, const uint8_t key[32]); void aes256_encrypt(const aes256_context *ctx, const aes256_block *in, aes256_block *out); void aes256_decrypt(const aes256_context *ctx, const aes256_block *in, aes256_block *out); #endif

提示:ek[60]大小是硬编码值,由 AES-256 轮数(14)决定:Nk=8(密钥字数),Nr=14(轮数),Nb=4(状态矩阵列数),密钥扩展总字数 =Nb * (Nr + 1) = 4 * 15 = 60。这个数字不能错,否则轮密钥加载会越界。

2.2 密钥扩展(Key Expansion):逐字生成 60 个轮密钥

密钥扩展是 AES 最易出错的环节之一。它将原始 32 字节密钥扩展为 60 个 32 位字(即 240 字节)的轮密钥序列。核心操作包括 RCON(轮常数)、SubWord(S-box 替换)、RotWord(循环左移)和异或。我们严格按 FIPS-197 第 5.2 节实现,不合并步骤、不跳过 RCON 异或、不省略字节序转换。

// aes256.c - key expansion snippet static const uint32_t rcon[14] = { 0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000, 0x1b000000, 0x36000000, 0x6c000000, 0xd8000000, 0xab000000, 0x4d000000 }; static uint32_t sub_word(uint32_t x) { return ((uint32_t)sbox[(x >> 24) & 0xFF] << 24) | ((uint32_t)sbox[(x >> 16) & 0xFF] << 16) | ((uint32_t)sbox[(x >> 8) & 0xFF] << 8) | ((uint32_t)sbox[x & 0xFF]); } static uint32_t rot_word(uint32_t x) { return ((x << 8) | (x >> 24)) & 0xFFFFFFFF; } void aes256_init(aes256_context *ctx, const uint8_t key[32]) { // Load first 8 words (256 bits) as initial key for (int i = 0; i < 8; i++) { ctx->ek[i] = ((uint32_t)key[4*i] << 24) | ((uint32_t)key[4*i+1] << 16) | ((uint32_t)key[4*i+2] << 8) | ((uint32_t)key[4*i+3]); } // Generate remaining 52 words (60 - 8 = 52) for (int i = 8; i < 60; i++) { uint32_t temp = ctx->ek[i-1]; if (i % 8 == 0) { temp = sub_word(rot_word(temp)) ^ rcon[i/8 - 1]; } else if (i % 8 == 4) { temp = sub_word(temp); } ctx->ek[i] = ctx->ek[i-8] ^ temp; } }

逻辑说明:

  • rcon数组是 AES-256 固定轮常数,从第 1 轮开始(对应i=8),i/8 - 1计算索引(如i=8 → index=0,i=16 → index=1);
  • sub_word(rot_word(temp))是每 8 字一轮的非线性变换,rot_word将0x01020304变为0x02030401,再经 S-box 替换;
  • i % 8 == 4时仅做sub_word(针对第 4、12、20... 字),这是 AES-256 特有的密钥扩展规则(因Nk=8,每 8 字触发一次完整变换);
  • 所有uint32_t按大端序(network byte order)加载密钥字节,确保跨平台一致性(x86/ARM/MIPS 均适用)。

2.3 加密主循环:14 轮正向变换的精确展开

AES 加密每轮包含四个步骤:AddRoundKey → SubBytes → ShiftRows → MixColumns(最后一轮省略 MixColumns)。我们用 T-tables(4 个 256 项 uint32_t 表)将 SubBytes+ShiftRows+MixColumns 合并为单次查表,大幅提升性能。T-tables 预计算公式见 FIPS-197 附录 A.2,此处直接给出已验证的常量表(为节省篇幅,仅列 T0 前 8 项,完整表见源码):

// Precomputed T-tables (generated offline, verified against NIST test vectors) static const uint32_t T0[256] = { 0xc66363a5U, 0xf87c7c84U, 0xee777799U, 0xf67b7b8dU, 0xfff2f20dU, 0xd66b6bbdU, 0xde6f6fb1U, 0x91c5c554U, // ... (248 more entries) }; // T1, T2, T3 defined similarly (omitted for brevity)

加密函数主体如下(以第 0 轮到第 13 轮为例):

void aes256_encrypt(const aes256_context *ctx, const aes256_block *in, aes256_block *out) { uint32_t s0, s1, s2, s3, t0, t1, t2, t3; uint32_t *ek = ctx->ek; // Initial AddRoundKey s0 = ((uint32_t)in->b[0] << 24) | ((uint32_t)in->b[1] << 16) | ((uint32_t)in->b[2] << 8) | ((uint32_t)in->b[3]); s1 = ((uint32_t)in->b[4] << 24) | ((uint32_t)in->b[5] << 16) | ((uint32_t)in->b[6] << 8) | ((uint32_t)in->b[7]); s2 = ((uint32_t)in->b[8] << 24) | ((uint32_t)in->b[9] << 16) | ((uint32_t)in->b[10] << 8) | ((uint32_t)in->b[11]); s3 = ((uint32_t)in->b[12] << 24)| ((uint32_t)in->b[13] << 16)| ((uint32_t)in->b[14] << 8) | ((uint32_t)in->b[15]); s0 ^= ek[0]; s1 ^= ek[1]; s2 ^= ek[2]; s3 ^= ek[3]; // Rounds 1 to 13: full round (AddRoundKey + T-tables) for (int r = 1; r < 14; r++) { t0 = T0[(s0 >> 24) & 0xFF] ^ T1[(s1 >> 16) & 0xFF] ^ T2[(s2 >> 8) & 0xFF] ^ T3[s3 & 0xFF] ^ ek[4*r]; t1 = T0[(s1 >> 24) & 0xFF] ^ T1[(s2 >> 16) & 0xFF] ^ T2[(s3 >> 8) & 0xFF] ^ T3[s0 & 0xFF] ^ ek[4*r+1]; t2 = T0[(s2 >> 24) & 0xFF] ^ T1[(s3 >> 16) & 0xFF] ^ T2[(s0 >> 8) & 0xFF] ^ T3[s1 & 0xFF] ^ ek[4*r+2]; t3 = T0[(s3 >> 24) & 0xFF] ^ T1[(s0 >> 16) & 0xFF] ^ T2[(s1 >> 8) & 0xFF] ^ T3[s2 & 0xFF] ^ ek[4*r+3]; s0 = t0; s1 = t1; s2 = t2; s3 = t3; } // Final round: AddRoundKey + SubBytes + ShiftRows (no MixColumns) t0 = ((uint32_t)sbox[(s0 >> 24) & 0xFF] << 24) | ((uint32_t)sbox[(s1 >> 16) & 0xFF] << 16) | ((uint32_t)sbox[(s2 >> 8) & 0xFF] << 8) | ((uint32_t)sbox[s3 & 0xFF]); t1 = ((uint32_t)sbox[(s1 >> 24) & 0xFF] << 24) | ((uint32_t)sbox[(s2 >> 16) & 0xFF] << 16) | ((uint32_t)sbox[(s3 >> 8) & 0xFF] << 8) | ((uint32_t)sbox[s0 & 0xFF]); t2 = ((uint32_t)sbox[(s2 >> 24) & 0xFF] << 24) | ((uint32_t)sbox[(s3 >> 16) & 0xFF] << 16) | ((uint32_t)sbox[(s0 >> 8) & 0xFF] << 8) | ((uint32_t)sbox[s1 & 0xFF]); t3 = ((uint32_t)sbox[(s3 >> 24) & 0xFF] << 24) | ((uint32_t)sbox[(s0 >> 16) & 0xFF] << 16) | ((uint32_t)sbox[(s1 >> 8) & 0xFF] << 8) | ((uint32_t)sbox[s2 & 0xFF]); s0 = t0 ^ ek[56]; // ek[56..59] is last round key s1 = t1 ^ ek[57]; s2 = t2 ^ ek[58]; s3 = t3 ^ ek[59]; // Store output in big-endian byte order out->b[0] = (s0 >> 24) & 0xFF; out->b[1] = (s0 >> 16) & 0xFF; out->b[2] = (s0 >> 8) & 0xFF; out->b[3] = s0 & 0xFF; out->b[4] = (s1 >> 24) & 0xFF; out->b[5] = (s1 >> 16) & 0xFF; out->b[6] = (s1 >> 8) & 0xFF; out->b[7] = s1 & 0xFF; out->b[8] = (s2 >> 24) & 0xFF; out->b[9] = (s2 >> 16) & 0xFF; out->b[10] = (s2 >> 8) & 0xFF; out->b[11] = s2 & 0xFF; out->b[12] = (s3 >> 24) & 0xFF; out->b[13] = (s3 >> 16) & 0xFF; out->b[14] = (s3 >> 8) & 0xFF; out->b[15] = s3 & 0xFF; }

参数说明:

  • s0~s3是状态矩阵的 4 行(每行 4 字节),按列优先顺序映射为 32 位字;
  • T-table 查表索引严格按s0>>24,s1>>16,s2>>8,s3&0xFF计算,对应 AES 状态矩阵的列混合输入顺序;
  • 最终轮ek[56..59]是第 14 轮密钥(ek[4*14]=ek[56]),索引计算必须精准;
  • 输出字节序强制大端(MSB first),与 NIST 测试向量格式完全一致,避免网络传输或文件存储时字节序混乱。

3. 解密实现与逆向轮函数:为什么不能简单“反向执行”加密流程?

AES 解密不是加密的逆序操作,而是对每轮变换求逆:InvSubBytes → InvShiftRows → InvMixColumns → AddRoundKey。若直接按加密流程倒推,需为每个步骤单独实现逆函数,代码膨胀且易错。更优解是采用“等效解密”(Equivalent Inverse Cipher),即将解密轮密钥重排,并复用加密的 T-tables 结构,仅替换为逆向 T-tables(T0'~T3')。这要求我们理解密钥调度的对称性:解密轮密钥并非简单倒序 ek[59]→ek[0],而是需重新计算或重排。

3.1 逆向密钥调度:重排而非倒序

AES-256 解密轮密钥不能直接用ek[59], ek[58], ..., ek[0],因为 AddRoundKey 在解密中仍位于每轮开头,而轮密钥需匹配逆向变换顺序。FIPS-197 规定:解密轮密钥dk[i]应满足dk[i] = ek[60 - i](i 从 0 开始),但需注意dk[0]对应第 14 轮密钥,dk[59]对应初始轮密钥。我们选择在线重排:解密时临时计算dk数组,避免额外存储。

// aes256.c - decryption key schedule (reordering) static void aes256_setup_decrypt_key(const aes256_context *ctx, uint32_t dk[60]) { // Copy and reverse the encryption key schedule for (int i = 0; i < 60; i++) { dk[i] = ctx->ek[59 - i]; } // Apply inverse MixColumns to all but first and last round keys // (FIPS-197 Section 5.3.3: only rounds 1..13 need InvMixColumns) for (int i = 4; i < 56; i += 4) { // skip round 0 (dk[0..3]) and round 14 (dk[56..59]) uint32_t k0 = dk[i], k1 = dk[i+1], k2 = dk[i+2], k3 = dk[i+3]; dk[i] = imc0(k0) ^ imc1(k1) ^ imc2(k2) ^ imc3(k3); dk[i+1] = imc0(k1) ^ imc1(k2) ^ imc2(k3) ^ imc3(k0); dk[i+2] = imc0(k2) ^ imc1(k3) ^ imc2(k0) ^ imc3(k1); dk[i+3] = imc0(k3) ^ imc1(k0) ^ imc2(k1) ^ imc3(k2); } } // imcX() are precomputed inverse MixColumns coefficients // e.g., imc0(x) = (0x0e * x0) ^ (0x0b * x1) ^ (0x0d * x2) ^ (0x09 * x3) mod x^4+1 // Implementation omitted (uses lookup tables for speed)

注意:imcX()函数需用伽罗瓦域乘法表实现,不可用普通整数乘法。我们提供已验证的 256 项imc0_tab[]等表,确保imc0(0x01)=0x0e,imc1(0x01)=0x0b等关系成立。

3.2 解密主循环:用逆向 T-tables(T0'~T3')复用结构

逆向 T-tables 将 InvSubBytes+InvShiftRows+InvMixColumns 合并。其构造比正向复杂,但原理相同:对每个字节输入,计算逆向变换结果。我们提供完整IT0[]~IT3[]表(256×4 项),解密循环结构与加密高度对称:

void aes256_decrypt(const aes256_context *ctx, const aes256_block *in, aes256_block *out) { uint32_t s0, s1, s2, s3, t0, t1, t2, t3; uint32_t dk[60]; aes256_setup_decrypt_key(ctx, dk); // recompute dk with InvMixColumns // Load ciphertext into state s0 = ((uint32_t)in->b[0] << 24) | ((uint32_t)in->b[1] << 16) | ((uint32_t)in->b[2] << 8) | ((uint32_t)in->b[3]); s1 = ((uint32_t)in->b[4] << 24) | ((uint32_t)in->b[5] << 16) | ((uint32_t)in->b[6] << 8) | ((uint32_t)in->b[7]); s2 = ((uint32_t)in->b[8] << 24) | ((uint32_t)in->b[9] << 16) | ((uint32_t)in->b[10] << 8) | ((uint32_t)in->b[11]); s3 = ((uint32_t)in->b[12] << 24)| ((uint32_t)in->b[13] << 16)| ((uint32_t)in->b[14] << 8) | ((uint32_t)in->b[15]); s0 ^= dk[0]; s1 ^= dk[1]; s2 ^= dk[2]; s3 ^= dk[3]; // Rounds 1 to 13: inverse full round for (int r = 1; r < 14; r++) { t0 = IT0[(s0 >> 24) & 0xFF] ^ IT1[(s3 >> 16) & 0xFF] ^ IT2[(s2 >> 8) & 0xFF] ^ IT3[s1 & 0xFF] ^ dk[4*r]; t1 = IT0[(s1 >> 24) & 0xFF] ^ IT1[(s0 >> 16) & 0xFF] ^ IT2[(s3 >> 8) & 0xFF] ^ IT3[s2 & 0xFF] ^ dk[4*r+1]; t2 = IT0[(s2 >> 24) & 0xFF] ^ IT1[(s1 >> 16) & 0xFF] ^ IT2[(s0 >> 8) & 0xFF] ^ IT3[s3 & 0xFF] ^ dk[4*r+2]; t3 = IT0[(s3 >> 24) & 0xFF] ^ IT1[(s2 >> 16) & 0xFF] ^ IT2[(s1 >> 8) & 0xFF] ^ IT3[s0 & 0xFF] ^ dk[4*r+3]; s0 = t0; s1 = t1; s2 = t2; s3 = t3; } // Final round: AddRoundKey + InvSubBytes + InvShiftRows t0 = ((uint32_t)isbox[(s0 >> 24) & 0xFF] << 24) | ((uint32_t)isbox[(s3 >> 16) & 0xFF] << 16) | ((uint32_t)isbox[(s2 >> 8) & 0xFF] << 8) | ((uint32_t)isbox[s1 & 0xFF]); t1 = ((uint32_t)isbox[(s1 >> 24) & 0xFF] << 24) | ((uint32_t)isbox[(s0 >> 16) & 0xFF] << 16) | ((uint32_t)isbox[(s3 >> 8) & 0xFF] << 8) | ((uint32_t)isbox[s2 & 0xFF]); t2 = ((uint32_t)isbox[(s2 >> 24) & 0xFF] << 24) | ((uint32_t)isbox[(s1 >> 16) & 0xFF] << 16) | ((uint32_t)isbox[(s0 >> 8) & 0xFF] << 8) | ((uint32_t)isbox[s3 & 0xFF]); t3 = ((uint32_t)isbox[(s3 >> 24) & 0xFF] << 24) | ((uint32_t)isbox[(s2 >> 16) & 0xFF] << 16) | ((uint32_t)isbox[(s1 >> 8) & 0xFF] << 8) | ((uint32_t)isbox[s0 & 0xFF]); s0 = t0 ^ dk[56]; s1 = t1 ^ dk[57]; s2 = t2 ^ dk[58]; s3 = t3 ^ dk[59]; // Store plaintext out->b[0] = (s0 >> 24) & 0xFF; out->b[1] = (s0 >> 16) & 0xFF; out->b[2] = (s0 >> 8) & 0xFF; out->b[3] = s0 & 0xFF; out->b[4] = (s1 >> 24) & 0xFF; out->b[5] = (s1 >> 16) & 0xFF; out->b[6] = (s1 >> 8) & 0xFF; out->b[7] = s1 & 0xFF; out->b[8] = (s2 >> 24) & 0xFF; out->b[9] = (s2 >> 16) & 0xFF; out->b[10] = (s2 >> 8) & 0xFF; out->b[11] = s2 & 0xFF; out->b[12] = (s3 >> 24) & 0xFF; out->b[13] = (s3 >> 16) & 0xFF; out->b[14] = (s3 >> 8) & 0xFF; out->b[15] = s3 & 0xFF; }

关键差异点:

  • 逆向查表索引顺序变为IT0[s0>>24] ^ IT1[s3>>16] ^ IT2[s2>>8] ^ IT3[s1&0xFF],反映 InvShiftRows 后的状态列顺序;
  • 最终轮 InvSubBytes+InvShiftRows 的字节映射为isbox[s0>>24]→out[0],isbox[s3>>16]→out[1]等,严格对应 AES 状态矩阵逆向行移位;
  • dk[56..59]是重排后的第 14 轮密钥,与加密ek[56..59]相同,无需额外计算。

4. 验证与测试:用 NIST 官方向量确认你的实现是否“真·正确”

写完加解密函数不等于实现正确。AES 是黑匣子,微小偏差(如 S-box 错一位、RCON 用错索引、字节序颠倒)会导致整个密文错乱,且难以定位。唯一可信的方式是用 NIST SP800-38A 附录 C 的官方测试向量(Test Vectors)进行逐字节比对。我们提供一个最小化测试框架,支持 ECB 模式下的 256 位密钥向量。

4.1 NIST ECB 测试向量解析与加载

NIST 提供的 AES-256-ECB 向量格式为:
KEY = 603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4
PLAINTEXT = 6bc1bee22e409f96e93d7e117393172a
CIPHERTEXT = f3eed1bdb5d2a03c064b5a7e3db1c2d2

注意:所有值均为十六进制字符串,需转换为字节数组。我们封装hexstr_to_bytes()辅助函数:

// utils.c int hexstr_to_bytes(const char *hex, uint8_t *out, size_t out_len) { size_t len = strlen(hex); if (len % 2 != 0 || len/2 > out_len) return -1; for (size_t i = 0; i < len; i += 2) { char byte_str[3] = {hex[i], hex[i+1], '\0'}; out[i/2] = (uint8_t)strtol(byte_str, NULL, 16); } return 0; }

4.2 自动化测试脚本:覆盖密钥、明文、密文三重校验

编写test_aes256.c,调用aes256_init()、aes256_encrypt()、aes256_decrypt(),并与 NIST 向量比对。关键点:必须测试加密后立即解密,验证可逆性;必须测试多组向量,包括边界密钥(全0、全F)。

// test_aes256.c #include "aes256.h" #include "utils.h" #include <stdio.h> #include <string.h> int main() { const char *key_hex = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; const char *pt_hex = "6bc1bee22e409f96e93d7e117393172a"; const char *ct_hex = "f3eed1bdb5d2a03c064b5a7e3db1c2d2"; uint8_t key[32], pt[16], ct[16], dec_pt[16]; aes256_context ctx; aes256_block in, out; // Load vectors if (hexstr_to_bytes(key_hex, key, 32) != 0 || hexstr_to_bytes(pt_hex, pt, 16) != 0 || hexstr_to_bytes(ct_hex, ct, 16) != 0) { printf("ERROR: hex parse failed\n"); return 1; } // Encrypt aes256_init(&ctx, key); memcpy(in.b, pt, 16); aes256_encrypt(&ctx, &in, &out); if (memcmp(out.b, ct, 16) != 0) { printf("ENCRYPT FAIL: expected "); print_bytes(ct, 16); printf(", got "); print_bytes(out.b, 16); printf("\n"); return 1; } printf("ENCRYPT PASS\n"); // Decrypt memcpy(in.b, ct, 16); aes256_decrypt(&ctx, &in, &out); if (memcmp(out.b, pt, 16) != 0) { printf("DECRYPT FAIL: expected "); print_bytes(pt, 16); printf(", got "); print_bytes(out.b, 16); printf("\n"); return 1; } printf("DECRYPT PASS\n"); // Round-trip test memcpy(in.b, pt, 16); aes256_encrypt(&ctx, &in, &out); aes256_decrypt(&ctx, &out, &in); if (memcmp(in.b, pt, 16) != 0) { printf("ROUND-TRIP FAIL\n"); return 1; } printf("ROUND-TRIP PASS\n"); return 0; }

提示:print_bytes()函数用于十六进制输出,便于人工核对。运行gcc -o test test_aes256.c aes256.c utils.c && ./test,输出

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询