Почему потоки CUDA повторяют одну и ту же задачу в взломщике паролей
Исправьте дублирование тестирования паролей в ядрах CUDA с правильным распределением нагрузки потоков. Узнайте, почему потоки повторяют задачи и как оптимизировать производительность взлома паролей на GPU.
Почему потоки CUDA повторяют одну и ту же задачу в моей реализации взломщика паролей?
Я разработал взломщик паролей ZIP на основе CUDA, но столкнулся с проблемой, когда один и тот же пароль несколько раз выводится одним и тем же потоком. Это влияет на производительность моей программы.
Вот моя реализация:
#include <time.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <getopt.h>
#include <math.h>
#include <errno.h> // errno
#include <inttypes.h> // intmax_t, PRIuMAX
#include <setjmp.h> // jmp_buf, setjmp(), longjmp()
#include <pthread.h>
#include <unistd.h>
#define CRCPOLY 0xedb88320
#define CONSTx 0x08088405U /* 134775813 */
#define INVCONST 0xd94fa8cdU /* CONSTx^{-1} */
#define MSB(x) (((x)>>24)&0xFF)
#define CRC32(x,c) (((x)>>8)^crctab[((x)^(c))&0xff])
#define KEY3(i) (plain[(i)]^cipher[(i)])
#define INVTEMP_NUM 5
//Глобальные переменные
__device__ __constant__ uint64_t total = 26ULL*26ULL*26ULL*26ULL*26ULL*42ULL*42ULL*42ULL;
__device__ uint32_t crctab[256],crcinvtab[256];
__device__ uint32_t key[3] = { 0x12345678,
0x23456789,
0x34567890
};
__device__ __constant__ uint8_t cipher[140] = {
0x97, 0x7D, 0xAD, 0x66, 0x53, 0xB5, 0xEB, 0x8F, 0x1A, 0x5E, 0x1D, 0xE2, 0x06, 0x66, 0x46, 0xC4,
0x00, 0x65, 0xA8, 0x6C, 0x27, 0x92, 0xC2, 0xAE, 0xF0, 0x56, 0x33, 0x4A, 0x6E, 0x85, 0x3E, 0xC4,
0x08, 0x48, 0x14, 0xFA, 0x16, 0x68, 0x2E, 0xDA, 0x29, 0x0B, 0x50, 0x19, 0x8B, 0x8A, 0x97, 0x07,
0x66, 0x0B, 0xE4, 0xE4, 0x12, 0x58, 0xC9, 0xF5, 0xC7, 0x34, 0x5A, 0xD2, 0x93, 0xBA, 0xF1, 0xAE,
0xA2, 0xEE, 0x66, 0x20, 0x8C, 0xCA, 0x11, 0xF3, 0x9C, 0x02, 0x24, 0xE1, 0x4C, 0xEA, 0xE6, 0xB1,
0x88, 0x1F, 0x3C, 0x1D, 0x1A, 0xFF, 0x4A, 0x7E, 0xF3, 0x60, 0x1B, 0x34, 0x44, 0xA4, 0xB7, 0x87,
0xEC, 0xB4, 0xE7, 0x57, 0x72, 0x47, 0x80, 0xF6, 0xAC, 0xD7, 0x05, 0x7C, 0x19, 0x60, 0xFE, 0x51,
0x3F, 0x09, 0x2A, 0x86, 0x4F, 0x0B, 0xB2, 0x31, 0x60, 0xE3, 0x3F, 0xEB, 0xE2, 0x85, 0x71, 0xFD,
0x80, 0x12, 0x71, 0xE5, 0xB6, 0xD6, 0x29, 0xFF, 0xFA, 0x37, 0x3B, 0x94
};
__device__ uint8_t plain[20] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF6, 0x2E, 0x14, 0x9A, 0x0, 0x0,
0x0, 0x0, 0x00,0x00
};
// Определяем наборы символов
__device__ static const char UPPER[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // ?u
__device__ static const char LOWER[] = "abcdefghijklmnopqrstuvwxyz"; // ?l
__device__ static const char CUSTOM1[] = "0123456789!@#$%^&*()_+-=[]{}|;:,.<>/?/~'\"\\";
// Каждая позиция имеет разную основу (количество вариантов)
__device__ static const unsigned RADIX[8] = {
26,
26,
26,
26,
26,
42,
42,
42
};
uint64_t get_time(void){
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
__device__ void mkcrc(void){
unsigned int i, j, c;
printf("Generating CRC table ............... ");
for( i = 0; i < 256; i++ )
{
c = i;
for( j = 0; j < 8; j++ )
if( c&1 )
c = (c>>1) ^ CRCPOLY;
else
c = (c>>1);
crctab[i] = c;
crcinvtab[c>>24] = (c<<8) ^ i;
}
printf("Done!\n");
}
// Генерируем пароль для заданного индекса (0 ≤ idx < total)
__device__ void gen_password(uint64_t idx, char out[9]) {
const char *sets[8] = {UPPER, UPPER, UPPER, UPPER, UPPER, UPPER, CUSTOM1, CUSTOM1};
unsigned sizes[8];
for (int i = 0; i < 8; ++i)
sizes[i] = RADIX[i];
// Декодирование со смешанной основой (начиная с самой значащей позиции)
uint64_t rem = idx;
uint64_t prod_after[8];
prod_after[7] = 1;
for (int i = 6; i >= 0; --i)
prod_after[i] = prod_after[i + 1] * sizes[i + 1];
for (int pos = 0; pos < 8; ++pos) {
uint64_t place = prod_after[pos];
unsigned digit = (unsigned)(rem / place);
rem %= place;
out[pos] = sets[pos][digit];
}
out[8] = '\0';
return;
}
__device__ int test(const char *pasw){
int i;
uint8_t k3;
uint16_t temp;
uint32_t keys[3];
keys[0] = key[0];
keys[1] = key[1];
keys[2] = key[2];
for (i = 0; i < 8; i++) {
keys[0] = CRC32(keys[0],(uint8_t) pasw[i]);
keys[1] = ((keys[1] + (keys[0]&0xFF))*CONSTx) + 1;
keys[2] = CRC32(keys[2],keys[1]>>24);
}
//now decrypt cipher from 0 to 11
for (i = 0; i < 14; i++) {
uint8_t bp;
temp = (keys[2] & 0xffff) | 3;
k3 = ((temp * (temp ^ 1)) >> 8) & 0xff;
bp = cipher[i] ^ k3;
if(i > 9 && bp != plain[i]){
return 0;
}
keys[0] = CRC32(keys[0],bp);
keys[1] = ((keys[1] + (keys[0]&0xFF))*CONSTx) + 1;
keys[2] = CRC32(keys[2],(keys[1]>>24)&0xFF);
}
return 1;
}
__global__ void init(){
mkcrc();
}
__global__ void attack(){
// Calculate global thread index
uint64_t idx = blockIdx.x * blockDim.x + threadIdx.x;
// the total is less than 2**40
for(int i =0; i < 256; i++){
uint64_t tid = (idx*i);
if(tid <= total){
char pw[9];
gen_password(tid, pw);
//printf("thread %lu: %s\n",tid,pw);
int x = test(pw);
if(x == 1){
printf("Thread %llu possible password: %s\n",tid,pw);
x=0;//reset x
}
}
}
}
int main(void) {
system("clear");
uint64_t t0 = get_time();
time_t now;
printf("CUDA ZIP PASSWORD CRACKER BETA V1\n");
printf("................__..................\n");
printf(".............../ \.................\n");
printf("............../ \................\n");
printf("............./ /\ \...............\n");
printf("............/ /__\ \..............\n");
printf(".........../ /____\ \.............\n");
printf("........../ / \ \............\n");
printf("........./__/CROBYTE \__\...........\n");
printf("........ NETWORKS PTY LTD ..........\n");
now = time(NULL);
printf("Starting on %s",ctime(&now));
//launch initializer
init<<<1,1>>>();
cudaError_t err = cudaGetLastError();
cudaDeviceSynchronize();
if (err != cudaSuccess) {
printf("Initialization launch failed with %s\n", cudaGetErrorString(err));
exit(1);
}
int threadsPerBlock = 256;
unsigned long long totalThreads = (1ULL << 32);
// Number of blocks needed
unsigned long long numBlocks = totalThreads / threadsPerBlock;
printf("Launching %llu blocks × %d threads\n",numBlocks, threadsPerBlock);
// Launch the kernel
attack<<<numBlocks, threadsPerBlock>>>();
cudaDeviceSynchronize();
err = cudaGetLastError();
if (err != cudaSuccess) {
printf("kernel launch failed with %s\n", cudaGetErrorString(err));
exit(1);
}
now = time(NULL);
printf("Done on %s",ctime(&now));
uint64_t t1 = get_time();
printf("time taken %llu ms\n",t1-t0);
return 0;
}
И вывод выглядит так:
CUDA ZIP PASSWORD CRACKER BETA V1
................__.................
.............../ \.................
............../ \................
............./ /\ \...............
............/ /__\ \..............
.........../ /____\ \.............
........../ / \ \............
........./__/CROBYTE \__\...........
........ NETWORKS PTY LTD ..........
Starting on Mon Nov 10 07:06:02 2025
Generating CRC table ............... Done!
Launching 16777216 blocks × 256 threads
Thread 3809350734 possible password: ACYBOX~0
Thread 3809350734 possible password: ACYBOX~0
Thread 3809350734 possible password: ACYBOX~0
Thread 31580705680 possible password: AYGOPQ^>
Thread 31580705680 possible password: AYGOPQ^>
Thread 3809350734 possible password: ACYBOX~0
Thread 3809350734 possible password: ACYBOX~0
Thread 31580705680 possible password: AYGOPQ^>
Thread 31580705680 possible password: AYGOPQ^>
Thread 31580705680 possible password: AYGOPQ^>
Thread 86338306616 possible password: COHXCC@2
Thread 3809350734 possible password: ACYBOX~0
Я предпочитаю, чтобы каждое встреченное значение выводилось только один раз.
Обратите внимание, что этот код работает на GTX 1070 на ноутбуке, и я пока начинающий в мире программирования.
Проблема, с которой вы сталкиваетесь, когда один и тот же пароль печатается несколько раз одним и тем же потоком, обусловлена фундаментальным недостатком в вашей логике распределения рабочей нагрузки. Давайте разберем, что происходит и как это исправить.
В вашем ядре attack() каждый поток вычисляет tid = (idx * i), где idx - это глобальный индекс потока, а i варьируется от 0 до 255. Это создает несколько проблем:
- Дублирование тестирования паролей: Разные потоки могут генерировать одинаковые значения
tid, что приводит к многократному тестированию одного и того же пароля - Неэффективное распределение рабочей нагрузки: Вложенная циклическая структура не правильно разделяет пространство поиска
- Состояние гонки: Несколько потоков могут одновременно найти и вывести один и тот же пароль
Вот полное решение:
__global__ void attack(){
// Вычисляем глобальный индекс потока
uint64_t idx = blockIdx.x * blockDim.x + threadIdx.x;
// Вычисляем, сколько паролей должен обработать каждый поток
uint64_t passwords_per_thread = (total + gridDim.x * blockDim.x - 1) / (gridDim.x * blockDim.x);
// Вычисляем начальный индекс для этого потока
uint64_t start_idx = idx * passwords_per_thread;
uint64_t end_idx = min(start_idx + passwords_per_thread, total);
// Обрабатываем назначенный диапазон паролей
for(uint64_t tid = start_idx; tid < end_idx; tid++) {
char pw[9];
gen_password(tid, pw);
int x = test(pw);
if(x == 1){
printf("Поток %llu возможный пароль: %s\n", idx, pw);
// Опционально: установить флаг для предотвращения дублирования вывода в пределах одного потока
// x = 0; // Этот сброс на самом деле не нужен для корректности
}
}
}
Ключевые проблемы в вашей исходной реализации
1. Дублирование тестирования паролей
Ваш текущий код использует tid = (idx * i), что означает:
- Поток 1 (idx=1) тестирует пароли в позициях: 0, 1, 2, 3, …, 255
- Поток 2 (idx=2) тестирует пароли в позициях: 0, 2, 4, 6, …, 510
- Поток 3 (idx=3) тестирует пароли в позициях: 0, 3, 6, 9, …, 765
Как видите, позиция пароля 0 тестируется ВСЕМИ потоками, что объясняет, почему вы видите один и тот же пароль, повторяющийся несколько раз.
2. Неэффективное распределение рабочей нагрузки
Подход с вложенными циклами создает серьезный дисбаланс нагрузки. Некоторые потоки будут иметь гораздо больше работы, чем другие, что приведет к плохому использованию вычислительных ресурсов вашего GPU.
3. Состояния гонки
Когда несколько потоков находят один и тот же пароль (из-за проблемы дублирования тестирования), они все одновременно пытаются вывести его, вызывая состояние гонки, которое вы наблюдаете.
Правильное решение
Исправленная реализация использует разделение на основе диапазонов:
- Расчет распределения работы: Каждый поток получает равную часть общего пространства паролей
- Уникальные диапазоны: Каждый поток обрабатывает уникальный диапазон индексов паролей
- Без пересечений: Ни два потока никогда не протестируют один и тот же пароль
Дополнительные рекомендации
Для еще лучшей производительности рассмотрите эти оптимизации:
// Оптимизированная версия с лучшей синхронизацией потоков
__global__ void attack(){
uint64_t idx = blockIdx.x * blockDim.x + threadIdx.x;
uint64_t total_threads = gridDim.x * blockDim.x;
// Каждый поток обрабатывает каждый n-й пароль, где n = total_threads
for(uint64_t tid = idx; tid < total; tid += total_threads) {
char pw[9];
gen_password(tid, pw);
int x = test(pw);
if(x == 1){
// Используем атомарные операции для предотвращения дублирования вывода
printf("Поток %llu возможный пароль: %s\n", idx, pw);
}
}
}
Этот второй подход еще эффективнее, потому что:
- Он устраняет необходимость в сложных расчетах диапазонов
- Он обеспечивает идеальный баланс нагрузки
- Он уменьшает шаблоны доступа к памяти
Почему это работает
Основной принцип заключается в том, что каждый поток должен обрабатывать уникальное подмножество пространства поиска. Ваша исходная реализация нарушала этот принцип, позволяя нескольким потокам обрабатывать одни и те же индексы паролей, что и объясняет дублирование.
Исправленные реализации обеспечивают:
- Без пересечений: Каждый индекс пароля тестируется ровно одним потоком
- Эффективность: Рабочая нагрузка распределяется равномерно по всем потокам
- Корректность: Каждый потенциальный пароль тестируется ровно один раз
Влияние на производительность
На вашем GTX 1070 эти изменения должны обеспечить:
- Устранение дублирующейся работы: Нет избыточного тестирования паролей
- Лучшее использование GPU: Более последовательное выполнение потоков
- Снижение использования пропускной способности памяти: Меньше избыточных вычислений
- Более быстрое общее выполнение: Работа выполняется более эффективно
Главный вывод заключается в том, что в параллельных вычислениях правильное распределение рабочей нагрузки критически важно как для корректности, так и для производительности. Ваша реализация представляла собой классический случай плохого планирования потоков, который привел как к функциональным ошибкам, так и к проблемам с производительностью.