827334870d
There were numerous library memory allocation inconsistencies. Some were checked for failure and others were not. All library source modules were modified to utilize the alloc.h memory rouines which are consistent in dealing with errors.
621 lines
17 KiB
C
621 lines
17 KiB
C
/*
|
|
* Copyright 1998-2003 by Albert Cahalan; all rights reserved.
|
|
* This file may be used subject to the terms and conditions of the
|
|
* GNU Library General Public License Version 2, or any later version
|
|
* at your option, as published by the Free Software Foundation.
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Library General Public License for more details.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <stdarg.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/utsname.h>
|
|
#include "procps.h"
|
|
#include "alloc.h"
|
|
#include "version.h"
|
|
#include "sysinfo.h" /* smp_num_cpus */
|
|
#include "wchan.h" // to verify prototypes
|
|
|
|
#define KSYMS_FILENAME "/proc/ksyms"
|
|
|
|
#if 0
|
|
#undef KSYMS_FILENAME
|
|
#define KSYMS_FILENAME "/would/be/nice/to/have/this/file"
|
|
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-hacked"
|
|
#define linux_version_code 131598 /* ? */
|
|
#define smp_num_cpus 2
|
|
#endif
|
|
|
|
#if 0
|
|
#undef KSYMS_FILENAME
|
|
#define KSYMS_FILENAME "/home/albert/ps/45621/ksyms-2.3.12"
|
|
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.12"
|
|
#define linux_version_code 131852 /* 2.3.12 */
|
|
#define smp_num_cpus 2
|
|
#endif
|
|
|
|
#if 0
|
|
#undef KSYMS_FILENAME
|
|
#define KSYMS_FILENAME "/home/albert/ps/45621/ksyms-2.3.18ac8-MODVERS"
|
|
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.18ac8-MODVERS"
|
|
#define linux_version_code 131858 /* 2.3.18ac8 */
|
|
#define smp_num_cpus 2
|
|
#endif
|
|
|
|
#if 0
|
|
#undef KSYMS_FILENAME
|
|
#define KSYMS_FILENAME "/home/albert/ps/45621/ksyms-2.3.18ac8-NOMODVERS"
|
|
#define SYSMAP_FILENAME "/home/albert/ps/45621/System.map-2.3.18ac8-NOMODVERS"
|
|
#define linux_version_code 131858 /* 2.3.18ac8 */
|
|
#define smp_num_cpus 2
|
|
#endif
|
|
|
|
/* These are the symbol types, with relative popularity:
|
|
* ? w machine type junk for Alpha -- odd syntax
|
|
* ? S not for i386
|
|
* 4 W not for i386
|
|
* 60 R
|
|
* 100 A
|
|
* 125 r
|
|
* 363 s not for i386
|
|
* 858 B
|
|
* 905 g generated by modutils?
|
|
* 929 G generated by modutils?
|
|
* 1301 b
|
|
* 2750 D
|
|
* 4481 d
|
|
* 11417 ?
|
|
* 13666 t
|
|
* 15442 T
|
|
*
|
|
* For i386, that is: "RArBbDd?tT"
|
|
*/
|
|
|
|
#define SYMBOL_TYPE_CHARS "Tt?dDbBrARGgsWS"
|
|
|
|
/*
|
|
* '?' is a symbol type
|
|
* '.' is part of a name (versioning?)
|
|
* "\t[]" are for the module name in /proc/ksyms
|
|
*/
|
|
#define LEGAL_SYSMAP_CHARS "0123456789_ ?.\n\t[]" \
|
|
"abcdefghijklmnopqrstuvwxyz" \
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
/* System.map lines look like:
|
|
* hex num, space, one of SYMBOL_TYPE_CHARS, space, LEGAL_SYSMAP_CHARS, \n
|
|
*
|
|
* Alpha systems can start with a few lines that have the address replaced
|
|
* by space padding and a 'w' for the type. For those lines, the last space
|
|
* is followed by something like: mikasa_primo_mv p2k_mv sable_gamma_mv
|
|
* (just one of those, always with a "_mv", then the newline)
|
|
*
|
|
* The /proc/ksyms lines are like System.map lines w/o the symbol type char.
|
|
* When odd features are used, the name part contains:
|
|
* "(.*)_R(smp_|smp2gig_|2gig_)?[0-9a-fA-F]{8,}"
|
|
* It is likely that more crap will be added...
|
|
*/
|
|
|
|
typedef struct symb {
|
|
unsigned KLONG addr;
|
|
const char *name;
|
|
} symb;
|
|
|
|
/* These mostly rely on POSIX to make them zero. */
|
|
|
|
static symb hashtable[256];
|
|
|
|
static char *sysmap_data;
|
|
static unsigned sysmap_room;
|
|
static symb *sysmap_index;
|
|
static unsigned sysmap_count;
|
|
|
|
static char *ksyms_data;
|
|
static unsigned ksyms_room = 4096;
|
|
static symb *ksyms_index;
|
|
static unsigned ksyms_count;
|
|
static unsigned idx_room;
|
|
|
|
/*********************************/
|
|
|
|
/* Kill this: _R(smp_?|smp2gig_?|2gig_?)?[0-9a-f]{8,}$
|
|
* We kill: (_R[^A-Z]*[0-9a-f]{8,})+$
|
|
*
|
|
* The loop should almost never be taken, but it has to be there.
|
|
* It gets rid of anything that _looks_ like a version code, even
|
|
* if a real version code has already been found. This is because
|
|
* the inability to perfectly recognize a version code may lead to
|
|
* symbol mangling, which in turn leads to mismatches between the
|
|
* /proc/ksyms and System.map data files.
|
|
*/
|
|
#if 0
|
|
static char *chop_version(char *arg){
|
|
char *cp;
|
|
cp = strchr(arg,'\t');
|
|
if(cp) *cp = '\0'; /* kill trailing module name first */
|
|
for(;;){
|
|
char *p;
|
|
int len = 0;
|
|
cp = strrchr(arg, 'R');
|
|
if(!cp || cp<=arg+1 || cp[-1]!='_') break;
|
|
for(p=cp; *++p; ){
|
|
switch(*p){
|
|
default:
|
|
goto out;
|
|
case '0' ... '9':
|
|
case 'a' ... 'f':
|
|
len++;
|
|
continue;
|
|
case 'g' ... 'z':
|
|
case '_':
|
|
len=0;
|
|
continue;
|
|
}
|
|
}
|
|
if(len<8) break;
|
|
cp[-1] = '\0';
|
|
}
|
|
out:
|
|
if(*arg=='G'){
|
|
int len = strlen(arg);
|
|
while( len>8 && !memcmp(arg,"GPLONLY_",8) ){
|
|
arg += 8;
|
|
len -= 8;
|
|
}
|
|
}
|
|
return arg;
|
|
}
|
|
#endif
|
|
static char *chop_version(char *arg){
|
|
char *cp;
|
|
cp = strchr(arg,'\t');
|
|
if(cp) *cp = '\0'; /* kill trailing module name first */
|
|
for(;;){
|
|
int len;
|
|
cp = strrchr(arg, 'R');
|
|
if(!cp || cp<=arg+1 || cp[-1]!='_') break;
|
|
len=strlen(cp);
|
|
if(len<9) break;
|
|
if(strpbrk(cp+1,"ABCDEFGHIJKLMNOPQRSTUVWXYZ")) break;
|
|
if(strspn(cp+len-8,"0123456789abcdef")!=8) break;
|
|
cp[-1] = '\0';
|
|
}
|
|
if(*arg=='G'){
|
|
int len = strlen(arg);
|
|
while( len>8 && !memcmp(arg,"GPLONLY_",8) ){
|
|
arg += 8;
|
|
len -= 8;
|
|
}
|
|
}
|
|
return arg;
|
|
}
|
|
|
|
/***********************************/
|
|
|
|
static const symb *search(unsigned KLONG address, symb *idx, unsigned count){
|
|
unsigned left;
|
|
unsigned mid;
|
|
unsigned right;
|
|
if(!idx) return NULL; /* maybe not allocated */
|
|
if(address < idx[0].addr) return NULL;
|
|
if(address >= idx[count-1].addr) return idx+count-1;
|
|
left = 0;
|
|
right = count-1;
|
|
for(;;){
|
|
mid = (left + right) / 2;
|
|
if(address >= idx[mid].addr) left = mid;
|
|
if(address <= idx[mid].addr) right = mid;
|
|
if(right-left <= 1) break;
|
|
}
|
|
if(address == idx[right].addr) return idx+right;
|
|
return idx+left;
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
/* allocate if needed, read, and return buffer size */
|
|
static void read_file(const char *restrict filename, char **bufp, unsigned *restrict roomp) {
|
|
int fd = 0;
|
|
ssize_t done;
|
|
char *buf = *bufp;
|
|
ssize_t total = 0;
|
|
unsigned room = *roomp;
|
|
|
|
if(!room) goto hell; /* failed before */
|
|
if(!buf) buf = xmalloc(room);
|
|
open_again:
|
|
fd = open(filename, O_RDONLY|O_NOCTTY|O_NONBLOCK);
|
|
if(fd<0){
|
|
switch(errno){
|
|
case EINTR: goto open_again;
|
|
default: _exit(101);
|
|
case EACCES: /* somebody screwing around? */
|
|
/* FIXME: set a flag to disable symbol lookup? */
|
|
case ENOENT:; /* no module support */
|
|
}
|
|
goto hell;
|
|
}
|
|
for(;;){
|
|
done = read(fd, buf+total, room-total-1);
|
|
if(done==0) break; /* nothing left */
|
|
if(done==-1){
|
|
if(errno==EINTR) continue; /* try again */
|
|
perror("");
|
|
goto hell;
|
|
}
|
|
if(done==(ssize_t)room-total-1){
|
|
char *tmp;
|
|
total += done;
|
|
/* more to go, but no room in buffer */
|
|
room *= 2;
|
|
tmp = xrealloc(buf, room);
|
|
buf = tmp;
|
|
continue;
|
|
}
|
|
if(done>0 && done<(ssize_t)room-total-1){
|
|
total += done;
|
|
continue; /* OK, we read some. Go do more. */
|
|
}
|
|
fprintf(stderr,"%ld can't happen\n", (long)done);
|
|
/* FIXME: memory leak */
|
|
_exit(42);
|
|
}
|
|
buf[total] = '\0'; // parse_ksyms() expects NUL-terminated file
|
|
*bufp = buf;
|
|
*roomp = room;
|
|
close(fd);
|
|
return;
|
|
hell:
|
|
if(buf) free(buf);
|
|
*bufp = NULL;
|
|
*roomp = 0; /* this function will never work again */
|
|
total = 0;
|
|
if(fd>0) close(fd);
|
|
return;
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
static int parse_ksyms(void) {
|
|
char *endp;
|
|
if(!ksyms_room || !ksyms_data) goto quiet_goodbye;
|
|
endp = ksyms_data;
|
|
ksyms_count = 0;
|
|
if(idx_room) goto bypass; /* some space already allocated */
|
|
idx_room = 512;
|
|
for(;;){
|
|
void *vp;
|
|
idx_room *= 2;
|
|
vp = xrealloc(ksyms_index, sizeof(symb)*idx_room);
|
|
ksyms_index = vp;
|
|
bypass:
|
|
for(;;){
|
|
char *saved;
|
|
if(!*endp) return 1;
|
|
saved = endp;
|
|
ksyms_index[ksyms_count].addr = STRTOUKL(endp, &endp, 16);
|
|
if(endp==saved || *endp != ' ') goto bad_parse;
|
|
endp++;
|
|
saved = endp;
|
|
endp = strchr(endp,'\n');
|
|
if(!endp) goto bad_parse; /* no newline */
|
|
*endp = '\0';
|
|
ksyms_index[ksyms_count].name = chop_version(saved);
|
|
++endp;
|
|
if(++ksyms_count >= idx_room) break; /* need more space */
|
|
}
|
|
}
|
|
|
|
if(0){
|
|
bad_parse:
|
|
fprintf(stderr, "Warning: "KSYMS_FILENAME" not normal\n");
|
|
}
|
|
quiet_goodbye:
|
|
idx_room = 0;
|
|
if(ksyms_data) free(ksyms_data) , ksyms_data = NULL;
|
|
ksyms_room = 0;
|
|
if(ksyms_index) free(ksyms_index) , ksyms_index = NULL;
|
|
ksyms_count = 0;
|
|
return 0;
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
#define VCNT 16
|
|
|
|
static int sysmap_mmap(const char *restrict const filename, message_fn message) {
|
|
struct stat sbuf;
|
|
char *endp;
|
|
int fd;
|
|
char Version[32];
|
|
fd = open(filename, O_RDONLY|O_NOCTTY|O_NONBLOCK);
|
|
if(fd<0) return 0;
|
|
if(fstat(fd, &sbuf) < 0) goto bad_open;
|
|
if(!S_ISREG(sbuf.st_mode)) goto bad_open;
|
|
if(sbuf.st_size < 5000) goto bad_open; /* if way too small */
|
|
/* Would be shared read-only, but we want '\0' after each name. */
|
|
endp = mmap(0, sbuf.st_size + 1, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
|
|
sysmap_data = endp;
|
|
while(*endp==' '){ /* damn Alpha machine types */
|
|
if(strncmp(endp," w ", 19)) goto bad_parse;
|
|
endp += 19;
|
|
endp = strchr(endp,'\n');
|
|
if(!endp) goto bad_parse; /* no newline */
|
|
if(strncmp(endp-3, "_mv\n", 4)) goto bad_parse;
|
|
endp++;
|
|
}
|
|
if(sysmap_data == (caddr_t) -1) goto bad_open;
|
|
close(fd);
|
|
fd = -1;
|
|
sprintf(Version, "Version_%d", linux_version_code);
|
|
sysmap_room = 512;
|
|
for(;;){
|
|
void *vp;
|
|
sysmap_room *= 2;
|
|
vp = xrealloc(sysmap_index, sizeof(symb)*sysmap_room);
|
|
sysmap_index = vp;
|
|
for(;;){
|
|
char *vstart;
|
|
if(endp - sysmap_data >= sbuf.st_size){ /* if we reached the end */
|
|
int i = VCNT; /* check VCNT times to verify this file */
|
|
if(*Version) goto bad_version;
|
|
if(!ksyms_index) return 1; /* if can not verify, assume success */
|
|
while(i--){
|
|
#if 1
|
|
const symb *findme;
|
|
const symb *map_symb;
|
|
/* Choose VCNT entries from /proc/ksyms to test */
|
|
findme = ksyms_index + (ksyms_count*i/VCNT);
|
|
/* Search for them in the System.map */
|
|
map_symb = search(findme->addr, sysmap_index, sysmap_count);
|
|
if(map_symb){
|
|
if(map_symb->addr != findme->addr) continue;
|
|
/* backup to first matching address */
|
|
while (map_symb != sysmap_index){
|
|
if (map_symb->addr != (map_symb-1)->addr) break;
|
|
map_symb--;
|
|
}
|
|
/* search for name in symbols with same address */
|
|
while (map_symb != (sysmap_index+sysmap_count)){
|
|
if (map_symb->addr != findme->addr) break;
|
|
if (!strcmp(map_symb->name,findme->name)) goto good_match;
|
|
map_symb++;
|
|
}
|
|
map_symb--; /* backup to last symbol with matching address */
|
|
message("{%s} {%s}\n",map_symb->name,findme->name);
|
|
goto bad_match;
|
|
}
|
|
good_match:;
|
|
#endif
|
|
}
|
|
return 1; /* success */
|
|
}
|
|
sysmap_index[sysmap_count].addr = STRTOUKL(endp, &endp, 16);
|
|
if(*endp != ' ') goto bad_parse;
|
|
endp++;
|
|
if(!strchr(SYMBOL_TYPE_CHARS, *endp)) goto bad_parse;
|
|
endp++;
|
|
if(*endp != ' ') goto bad_parse;
|
|
endp++;
|
|
vstart = endp;
|
|
endp = strchr(endp,'\n');
|
|
if(!endp) goto bad_parse; /* no newline */
|
|
*endp = '\0';
|
|
++endp;
|
|
vstart = chop_version(vstart);
|
|
sysmap_index[sysmap_count].name = vstart;
|
|
if(*vstart=='V' && *Version && !strcmp(Version,vstart)) *Version='\0';
|
|
if(++sysmap_count >= sysmap_room) break; /* need more space */
|
|
}
|
|
}
|
|
|
|
if(0){
|
|
bad_match:
|
|
message("Warning: %s does not match kernel data.\n", filename);
|
|
}
|
|
if(0){
|
|
bad_version:
|
|
message("Warning: %s has an incorrect kernel version.\n", filename);
|
|
}
|
|
if(0){
|
|
bad_parse:
|
|
message("Warning: %s not parseable as a System.map\n", filename);
|
|
}
|
|
if(0){
|
|
bad_open:
|
|
message("Warning: %s could not be opened as a System.map\n", filename);
|
|
}
|
|
|
|
sysmap_room=0;
|
|
sysmap_count=0;
|
|
if(sysmap_index) free(sysmap_index);
|
|
sysmap_index = NULL;
|
|
if(fd>=0) close(fd);
|
|
if(sysmap_data) munmap(sysmap_data, sbuf.st_size + 1);
|
|
sysmap_data = NULL;
|
|
return 0;
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
static void read_and_parse(void){
|
|
static time_t stamp; /* after data gets old, load /proc/ksyms again */
|
|
if(time(NULL) != stamp){
|
|
read_file(KSYMS_FILENAME, &ksyms_data, &ksyms_room);
|
|
parse_ksyms();
|
|
memset((void*)hashtable,0,sizeof(hashtable)); /* invalidate cache */
|
|
stamp = time(NULL);
|
|
}
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
static void default_message(const char *restrict format, ...) __attribute__((format(printf,1,2)));
|
|
static void default_message(const char *restrict format, ...) {
|
|
va_list arg;
|
|
|
|
va_start (arg, format);
|
|
vfprintf (stderr, format, arg);
|
|
va_end (arg);
|
|
}
|
|
|
|
/*********************************/
|
|
|
|
static int use_wchan_file;
|
|
|
|
int open_psdb_message(const char *restrict override, message_fn message) {
|
|
static const char *sysmap_paths[] = {
|
|
"/boot/System.map-%s",
|
|
"/boot/System.map",
|
|
"/lib/modules/%s/System.map",
|
|
"/usr/src/linux/System.map",
|
|
"/System.map",
|
|
NULL
|
|
};
|
|
struct stat sbuf;
|
|
struct utsname uts;
|
|
char path[128];
|
|
const char **fmt = sysmap_paths;
|
|
const char *sm;
|
|
|
|
#ifdef SYSMAP_FILENAME /* debug feature */
|
|
override = SYSMAP_FILENAME;
|
|
#endif
|
|
|
|
// first allow for a user-selected System.map file
|
|
if(
|
|
(sm=override)
|
|
||
|
|
(sm=getenv("PS_SYSMAP"))
|
|
||
|
|
(sm=getenv("PS_SYSTEM_MAP"))
|
|
){
|
|
if(!have_privs){
|
|
read_and_parse();
|
|
if(sysmap_mmap(sm, message)) return 0;
|
|
}
|
|
/* failure is better than ignoring the user & using bad data */
|
|
return -1; /* ought to return "Namelist not found." */
|
|
}
|
|
|
|
// next try the Linux 2.5.xx method
|
|
if(!stat("/proc/self/wchan", &sbuf)){
|
|
use_wchan_file = 1; // hack
|
|
return 0;
|
|
}
|
|
|
|
// finally, search for the System.map file
|
|
uname(&uts);
|
|
path[sizeof path - 1] = '\0';
|
|
do{
|
|
int did_ksyms = 0;
|
|
snprintf(path, sizeof path - 1, *fmt, uts.release);
|
|
if(!stat(path, &sbuf)){
|
|
if (did_ksyms++) read_and_parse();
|
|
if (sysmap_mmap(path, message)) return 0;
|
|
}
|
|
}while(*++fmt);
|
|
/* TODO: Without System.map, no need to keep ksyms loaded. */
|
|
return -1;
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
int open_psdb(const char *restrict override) {
|
|
return open_psdb_message(override, default_message);
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
static const char * read_wchan_file(unsigned pid){
|
|
static char buf[64];
|
|
const char *ret = buf;
|
|
ssize_t num;
|
|
int fd;
|
|
|
|
snprintf(buf, sizeof buf, "/proc/%d/wchan", pid);
|
|
fd = open(buf, O_RDONLY);
|
|
if(fd==-1) return "?";
|
|
num = read(fd, buf, sizeof buf - 1);
|
|
close(fd);
|
|
if(num<1) return "?"; // allow for "0"
|
|
buf[num] = '\0';
|
|
|
|
if(buf[0]=='0' && buf[1]=='\0') return "-";
|
|
|
|
// would skip over numbers if they existed -- but no
|
|
|
|
// lame ppc64 has a '.' in front of every name
|
|
if(*ret=='.') ret++;
|
|
switch(*ret){
|
|
case 's': if(!strncmp(ret, "sys_", 4)) ret += 4; break;
|
|
case 'd': if(!strncmp(ret, "do_", 3)) ret += 3; break;
|
|
case '_': while(*ret=='_') ret++; break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
static const symb fail = { .name = "?" };
|
|
static const char dash[] = "-";
|
|
static const char star[] = "*";
|
|
|
|
#define MAX_OFFSET (0x1000*sizeof(long)) /* past this is generally junk */
|
|
|
|
/* return pointer to temporary static buffer with function name */
|
|
const char * lookup_wchan(unsigned KLONG address, unsigned pid) {
|
|
const symb *mod_symb;
|
|
const symb *map_symb;
|
|
const symb *good_symb;
|
|
const char *ret;
|
|
unsigned hash;
|
|
|
|
// can't cache it due to a race condition :-(
|
|
if(use_wchan_file) return read_wchan_file(pid);
|
|
|
|
if(!address) return dash;
|
|
if(!~address) return star;
|
|
|
|
read_and_parse();
|
|
hash = (address >> 4) & 0xff; /* got 56/63 hits & 7/63 misses */
|
|
if(hashtable[hash].addr == address) return hashtable[hash].name;
|
|
mod_symb = search(address, ksyms_index, ksyms_count);
|
|
if(!mod_symb) mod_symb = &fail;
|
|
map_symb = search(address, sysmap_index, sysmap_count);
|
|
if(!map_symb) map_symb = &fail;
|
|
|
|
/* which result is closest? */
|
|
good_symb = (mod_symb->addr > map_symb->addr)
|
|
? mod_symb
|
|
: map_symb
|
|
;
|
|
if(address > good_symb->addr + MAX_OFFSET) good_symb = &fail;
|
|
|
|
/* good_symb->name has the data, but needs to be trimmed */
|
|
ret = good_symb->name;
|
|
// lame ppc64 has a '.' in front of every name
|
|
if(*ret=='.') ret++;
|
|
switch(*ret){
|
|
case 's': if(!strncmp(ret, "sys_", 4)) ret += 4; break;
|
|
case 'd': if(!strncmp(ret, "do_", 3)) ret += 3; break;
|
|
case '_': while(*ret=='_') ret++; break;
|
|
}
|
|
/* if(!*ret) ret = fail.name; */ /* not likely (name was "sys_", etc.) */
|
|
|
|
/* cache name after abbreviation */
|
|
hashtable[hash].addr = address;
|
|
hashtable[hash].name = ret;
|
|
|
|
return ret;
|
|
}
|